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/4] 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 0000000..c13dcfc --- /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 0000000..e123e4e --- /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 0000000..115c899 --- /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 0000000..2f20179 --- /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 0000000..51db7a6 --- /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 0000000..3e55927 --- /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 0000000..6d0f162 --- /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 0000000..e666d46 --- /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/4] 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 267ff44..83c313c 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 917a265..2c872fc 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 d06658f..2161187 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 f4ac69b..7661d0f 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 955517e..b330bf3 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 33c7139..0391980 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 7440626..6b98768 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 e9d45b6..5f75725 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 5f44020..f69d5ba 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 ef883ce..18c2523 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 263b57f..89efd57 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 ffbe12e..67923a4 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 d912f1b..ee6d3e0 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 2f6a38c..f25f4bf 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 96dcb71..4a93697 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 5fb1683..6dfb5d8 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 320f741..3e54fa0 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 16307de..da8b8e8 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 da09e22..c474345 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 4f4720e..86073fe 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 4241d08..e8d5c8d 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 9e33bc6..33e6ff0 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 00e523c..d7851f5 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 16e6c69..c8433a4 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 0cf10c6..c76bfcd 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 05c6770..199baa4 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 782cc5f..2ea8b51 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 19bc7d9..7d95013 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 96e1b43..0410bdc 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 569a972..4858cf5 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 facc723..c1a0da7 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 4c3530e..e0159e1 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 f5a79bc..529dd12 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 782cc5f..2ea8b51 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 bc9d040..037ce09 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 a7cf79b..134b0a8 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 844664a..7c427de 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 b03451e..9a6ac28 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 69dc41c..b724a65 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 daffa2d..49adec5 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 7f2dc1e..d117e74 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 fa33793..70f2741 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 87960c4..8508fe8 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 08f3c4b..9af9a28 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 782cc5f..2ea8b51 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 e4185ed..94dc8f2 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 6ffc8d7..dbc96f4 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 446cd32..3b410b1 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 b864fb9..83da948 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 8b415f6..a983ab3 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 85dcd21..320c268 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 d70f54d..9a24e10 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 3719eb7..982a7c1 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 f7ee86d..177a9f7 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 89bd087..e45d899 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 7770fd2..95153b5 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 782cc5f..2ea8b51 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 9eecc59..f0d0b0a 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 3c43943..8c5e8c1 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 ebe3cea..8861af1 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 fe4a2a6..a020ec8 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 782cc5f..2ea8b51 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 4d36128..60feac4 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 54f6c08..a0e7010 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 631eb57..34b46e9 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 9ee256e..51ac516 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 2f0783d..873beca 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 bf94e6c..c6b6771 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 4474f6d..6d754e5 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 643f0b7..0dd3436 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 6f84d66..f1bf824 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 5404982..cd95b2f 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 1eac3bb..9ecadea 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 7e5ce51..e3c29d5 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 67fe62e..1aefea3 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 782cc5f..2ea8b51 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 20587d3..e031330 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 993c3a7..6b9d319 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 7e5ce51..e3c29d5 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 27e3ee7..a4a4b50 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 4d60fc1..45e97c6 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 d53ee43..90b59e6 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 a764a2f..bca38ba 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 518e3a2..f2df506 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 da4b06f..25ffadc 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 bf58fc7..c93c46b 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 7e5ce51..e3c29d5 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 17d4618..73baec5 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 16eaf11..3f7e70b 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 7239dd5..f130269 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 9f03cab..d2ee9da 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 4d60fc1..45e97c6 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 b524352..ce7eaca 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 708194b..9512cfb 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 4c625b4..0e99d2c 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 e95a721..a3249ef 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 8225eb1..286f007 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 2847fca..1c35ab2 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 b414238..96c94b9 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 f28464c..525777f 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 c882b85..f4d033b 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 eeb2aee..c04ad84 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 6d9a4be..fbab611 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 1867292..1ec3b7f 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 08cc728..988e25f 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 3dc4390..8687e46 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 a27b0ff..b9c7132 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 ff922d8..8c0ff22 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 643f0b7..0dd3436 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 19484e4..c93e262 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 12b16a3..e6a7dda 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 0857d59..a0f2df1 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 11f62ca..69ca193 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 6cbf9d5..be5a3c2 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 8feff65..b0adce5 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 503e7da..c122434 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 7f20069..f12edae 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 643f0b7..0dd3436 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 6e0eb01..b3b7eb6 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 6c8c435..c4a38b5 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 0c6fa03..bfb99c5 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 4d60fc1..45e97c6 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 cd75cad..6ed0a44 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 4725cb9..fffaa54 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 95163a6..5ca7be7 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 fd6bd35..61c83a2 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 ad018c6..55ece5d 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 9b1125c..dec3253 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 3654e12..b4cc98d 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 8457ce0..9575e27 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 86b24de..17902ee 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 a913c5b..e836348 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 7a115e0..8828693 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 ef7a657..deb04a9 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 81fda9a..3f1df53 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 9cd9486..c93355d 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 2670bc5..dbded3f 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 d391659..2d59b7f 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 38a58f1..2369509 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 5ae8d03..832475a 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 2180c7c..3692696 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 89f7984..14e0968 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 ec073b1..2c11cdf 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 6c261f9..6730d55 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 369e439..9a68dc2 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 ccd85ac..656fc82 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 dad18a9..adc955c 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 685e1e5..cbb2740 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 393a4ca..d735894 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 e6261d4..6860812 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 76f1f1a..ab6552b 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 66e86be..928b162 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 dbc0458..691a8ef 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 136710d..a7ef50b 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 110dc84..0032086 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 ad3b940..6490b64 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 c0e1e9c..baf4d2e 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 c96f8aa..239ae14 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 2a83f8a..816265a 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 8292193..3907350 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 63334ad..ccdcf84 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 bdcfdc8..b90ed51 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 80fd649..bffceda 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 d7723f4..4ba3c88 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 643f0b7..0dd3436 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 c609ebb..6b4a4b0 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 0853d1f..d494174 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 45f5f73..b8022d7 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 e648dee..8c586b7 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 12d1027..aa75f50 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 dbdf1b6..cd26450 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 6e44f68..3043f25 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 1643d24..cd16a4d 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 b25a4ff..7b1090c 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 01093a7..fc99dc6 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 b8178cf..d1dedec 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 9c42738..36a54e2 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 643f0b7..0dd3436 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 404f106..5dc98c2 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 1d68a8c..6514897 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 8dee83d..3a0c702 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 38ed635..8095051 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 030e54c..329d85b 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 a53122b..15a1a9d 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 217df51..3579901 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 aae3410..5b73bed 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 e23b53b..4b04009 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 782cc5f..2ea8b51 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 8878f39..80ed8a9 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 4dfea3f..b36d70e 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 8296591..350d44e 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 0082536..68e20eb 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 643f0b7..0dd3436 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 de25a156135411193c68e2ce0161774e2b7dda40 Mon Sep 17 00:00:00 2001 From: Liber Date: Sun, 9 Aug 2026 23:48:33 +0800 Subject: [PATCH 3/4] feat(bug): add markdown description and fix empty handler dropdown (#138) Add an optional markdown `description` to bugs, end-to-end: - migration 0111 adds the `description` column to ms_bug - domain NewBug/Bug carry description; create/update use cases and the pg/in_memory/http adapters persist and return it - web: MarkdownEditor for create/edit, MarkdownRenderer in a read-only detail drawer opened on row click Fix the empty assignee dropdown: useMemberOptions now merges project members with the global user directory, so a handler can always be selected even when a project has no members. Handler names link to /user. --- crates/bug/src/adapters/http.rs | 10 ++ crates/bug/src/adapters/in_memory.rs | 3 + crates/bug/src/adapters/pg.rs | 19 +++- crates/bug/src/application/change_status.rs | 11 +- crates/bug/src/application/create_bug.rs | 26 +++-- crates/bug/src/application/custom_fields.rs | 2 +- crates/bug/src/application/followers.rs | 11 +- crates/bug/src/application/list_bugs.rs | 33 +++++- crates/bug/src/application/relations.rs | 11 +- crates/bug/src/application/update_meta.rs | 19 +++- crates/bug/src/domain/bug.rs | 12 ++ crates/bug/src/ports/bug_repository.rs | 6 +- .../migrations/0111_bug_description.sql | 2 + web/src/api.ts | 8 +- web/src/pages/Bugs.tsx | 106 +++++++++++++++--- 15 files changed, 235 insertions(+), 44 deletions(-) create mode 100644 crates/migrate/migrations/0111_bug_description.sql diff --git a/crates/bug/src/adapters/http.rs b/crates/bug/src/adapters/http.rs index d117e74..02e503f 100644 --- a/crates/bug/src/adapters/http.rs +++ b/crates/bug/src/adapters/http.rs @@ -109,6 +109,9 @@ struct BugResponse { /// Handler user id. #[serde(skip_serializing_if = "Option::is_none")] handler: Option, + /// Markdown description body (defect details). + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, /// Last mutator's user id (stamped on every mutation). #[serde(skip_serializing_if = "Option::is_none")] updated_by: Option, @@ -131,6 +134,7 @@ impl From for BugResponse { created_by: b.created_by, severity: b.severity, handler: b.handler, + description: b.description, updated_by: b.updated_by, updated_at: b.updated_at, custom_fields: b.custom_fields, @@ -148,6 +152,8 @@ struct CreateBugRequest { severity: Option, /// Handler user id (optional). handler: Option, + /// Markdown description body (optional). + description: Option, /// Custom field values, key → string value (max 32 keys, key ≤ 64 chars, value ≤ 2000 chars). #[serde(default)] custom_fields: BTreeMap, @@ -171,6 +177,7 @@ async fn create_bug( Some(&user.user_id), req.severity.as_deref(), req.handler.as_deref(), + req.description.as_deref(), &req.custom_fields, ) .await @@ -261,6 +268,8 @@ struct UpdateBugRequest { severity: Option, /// Handler user id; absent clears. handler: Option, + /// Markdown description body; absent keeps the current value. + description: Option, } /// Updates title/severity/handler and stamps updated_by/updated_at with the caller. @@ -281,6 +290,7 @@ async fn update_bug( req.title.as_deref(), req.severity.as_deref(), req.handler.as_deref(), + req.description.as_deref(), Some(&user.user_id), ) .await diff --git a/crates/bug/src/adapters/in_memory.rs b/crates/bug/src/adapters/in_memory.rs index 4cce826..c825557 100644 --- a/crates/bug/src/adapters/in_memory.rs +++ b/crates/bug/src/adapters/in_memory.rs @@ -82,6 +82,7 @@ impl BugRepository for InMemoryBugRepository { created_by: new_bug.created_by.clone(), severity: new_bug.severity.clone(), handler: new_bug.handler.clone(), + description: new_bug.description.clone(), updated_by: new_bug.created_by.clone(), updated_at: Some(tick(seq)), custom_fields: new_bug.custom_fields.clone(), @@ -136,6 +137,7 @@ impl BugRepository for InMemoryBugRepository { title: &str, severity: Option<&str>, handler: Option<&str>, + description: Option<&str>, operator: Option<&str>, ) -> Result, RepoError> { let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner); @@ -145,6 +147,7 @@ impl BugRepository for InMemoryBugRepository { b.title = title.to_string(); b.severity = severity.map(str::to_string); b.handler = handler.map(str::to_string); + b.description = description.map(str::to_string); b.updated_by = operator.map(str::to_string); b.updated_at = Some(tick(seq)); b.clone() diff --git a/crates/bug/src/adapters/pg.rs b/crates/bug/src/adapters/pg.rs index 70f2741..6c93286 100644 --- a/crates/bug/src/adapters/pg.rs +++ b/crates/bug/src/adapters/pg.rs @@ -45,7 +45,7 @@ fn json_to_fields(v: &serde_json::Value) -> BTreeMap { } const BUG_COLS: &str = "id, project_id, title, status, deleted, created_at, created_by, \ - severity, handler, updated_by, updated_at::text AS updated_at, custom_fields"; + severity, handler, description, updated_by, updated_at::text AS updated_at, custom_fields"; fn row_to_bug(row: &sqlx::postgres::PgRow) -> Result { let custom: serde_json::Value = row.try_get("custom_fields").map_err(map_err)?; @@ -59,6 +59,7 @@ fn row_to_bug(row: &sqlx::postgres::PgRow) -> Result { created_by: row.try_get("created_by").map_err(map_err)?, severity: row.try_get("severity").map_err(map_err)?, handler: row.try_get("handler").map_err(map_err)?, + description: row.try_get("description").map_err(map_err)?, updated_by: row.try_get("updated_by").map_err(map_err)?, updated_at: row.try_get("updated_at").map_err(map_err)?, custom_fields: json_to_fields(&custom), @@ -108,8 +109,8 @@ impl BugRepository for PgBugRepository { // updated_by/updated_at seed from the creator so the audit pair is never empty. let row = sqlx::query(&format!( "INSERT INTO ms_bug (project_id, title, status, created_by, custom_fields, \ - severity, handler, updated_by, updated_at) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $4, now()) RETURNING {BUG_COLS}" + severity, handler, description, updated_by, updated_at) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $4, now()) RETURNING {BUG_COLS}" )) .bind(&new_bug.project_id) .bind(&new_bug.title) @@ -118,6 +119,7 @@ impl BugRepository for PgBugRepository { .bind(fields_to_json(&new_bug.custom_fields)) .bind(&new_bug.severity) .bind(&new_bug.handler) + .bind(&new_bug.description) .fetch_one(&self.pool) .await .map_err(map_err)?; @@ -171,10 +173,12 @@ impl BugRepository for PgBugRepository { title: &str, severity: Option<&str>, handler: Option<&str>, + description: Option<&str>, operator: Option<&str>, ) -> Result, RepoError> { let row = sqlx::query(&format!( "UPDATE ms_bug SET title = $2, severity = $3, handler = $4, \ + description = COALESCE($6, description), \ updated_by = $5, updated_at = now() \ WHERE id = $1 AND deleted = false RETURNING {BUG_COLS}" )) @@ -183,6 +187,7 @@ impl BugRepository for PgBugRepository { .bind(severity) .bind(handler) .bind(operator) + .bind(description) .fetch_optional(&self.pool) .await .map_err(map_err)?; @@ -368,7 +373,7 @@ mod tests { // Meta update replaces severity/handler and stamps the operator. let updated = repo - .update_meta(&bug.id, "login crash (modified)", Some("P0"), None, Some("dave")) + .update_meta(&bug.id, "login crash (modified)", Some("P0"), None, None, Some("dave")) .await .expect("meta") .expect("some"); @@ -376,7 +381,11 @@ mod tests { assert_eq!(updated.severity.as_deref(), Some("P0")); assert_eq!(updated.handler, None); assert_eq!(updated.updated_by.as_deref(), Some("dave")); - assert!(repo.update_meta("ghost", "x", None, None, None).await.expect("meta").is_none()); + assert!(repo + .update_meta("ghost", "x", None, None, None, None) + .await + .expect("meta") + .is_none()); // Full replacement; empty map clears all. repo.set_custom_fields(&bug.id, &cf(&[("env", "prod")]), Some("erin")) diff --git a/crates/bug/src/application/change_status.rs b/crates/bug/src/application/change_status.rs index 639548d..5170a56 100644 --- a/crates/bug/src/application/change_status.rs +++ b/crates/bug/src/application/change_status.rs @@ -52,7 +52,16 @@ mod tests { async fn seed_bug(repo: &InMemoryBugRepository) -> String { let create = CreateBugUseCase::new(Arc::new(repo.clone())); create - .execute("p1", "boom", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "boom", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("seed") .id diff --git a/crates/bug/src/application/create_bug.rs b/crates/bug/src/application/create_bug.rs index 8508fe8..e205041 100644 --- a/crates/bug/src/application/create_bug.rs +++ b/crates/bug/src/application/create_bug.rs @@ -40,6 +40,7 @@ impl CreateBugUseCase { created_by: Option<&str>, severity: Option<&str>, handler: Option<&str>, + description: Option<&str>, custom_fields: &BTreeMap, ) -> Result { let custom_fields = normalize_custom_fields(custom_fields)?; @@ -48,6 +49,7 @@ impl CreateBugUseCase { .with_created_by(created_by) .with_severity(severity) .with_handler(normalize_handler(handler)) + .with_description(description.map(|s| s.to_string())) .with_custom_fields(custom_fields); let flow = self.repo.status_flow(project_id).await?; @@ -71,7 +73,7 @@ mod tests { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); let bug = uc - .execute("p1", "login crash", "NEW", Some("alice"), None, None, &BTreeMap::new()) + .execute("p1", "login crash", "NEW", Some("alice"), None, None, None, &BTreeMap::new()) .await .expect("ok"); assert_eq!(bug.status, "NEW"); @@ -95,6 +97,7 @@ mod tests { None, Some(" p0 "), Some(" bob "), + None, &BTreeMap::new(), ) .await @@ -108,7 +111,7 @@ mod tests { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); let err = uc - .execute("p1", "x", "NEW", None, Some("SEV1"), None, &BTreeMap::new()) + .execute("p1", "x", "NEW", None, Some("SEV1"), None, None, &BTreeMap::new()) .await .unwrap_err(); assert_eq!(err, CreateBugError::Validation(BugError::InvalidSeverity("SEV1".into()))); @@ -119,12 +122,15 @@ 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", "login crash", "NEW", None, None, None, &raw).await.expect("ok"); + let bug = + uc.execute("p1", "login crash", "NEW", None, 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", "crashed again", "NEW", None, None, None, &bad).await.unwrap_err(), + uc.execute("p1", "crashed again", "NEW", None, None, None, None, &bad) + .await + .unwrap_err(), CreateBugError::Validation(BugError::EmptyCustomFieldKey) ); } @@ -133,8 +139,10 @@ mod tests { async fn rejects_unknown_initial_status() { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); - let err = - uc.execute("p1", "x", "GHOST", None, None, None, &BTreeMap::new()).await.unwrap_err(); + let err = uc + .execute("p1", "x", "GHOST", None, None, None, None, &BTreeMap::new()) + .await + .unwrap_err(); assert_eq!(err, CreateBugError::Validation(BugError::UnknownStatus("GHOST".into()))); } @@ -142,8 +150,10 @@ mod tests { async fn rejects_blank_title() { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); - let err = - uc.execute("p1", " ", "NEW", None, None, None, &BTreeMap::new()).await.unwrap_err(); + let err = uc + .execute("p1", " ", "NEW", None, None, None, None, &BTreeMap::new()) + .await + .unwrap_err(); assert_eq!(err, CreateBugError::Validation(BugError::EmptyTitle)); } } diff --git a/crates/bug/src/application/custom_fields.rs b/crates/bug/src/application/custom_fields.rs index 5132512..8433430 100644 --- a/crates/bug/src/application/custom_fields.rs +++ b/crates/bug/src/application/custom_fields.rs @@ -55,7 +55,7 @@ mod tests { async fn seed_bug(repo: &InMemoryBugRepository) -> String { CreateBugUseCase::new(Arc::new(repo.clone())) - .execute("p1", "boom", "NEW", None, None, None, &fields(&[("severity", "P1")])) + .execute("p1", "boom", "NEW", None, None, None, None, &fields(&[("severity", "P1")])) .await .expect("seed") .id diff --git a/crates/bug/src/application/followers.rs b/crates/bug/src/application/followers.rs index 1742e5c..653f2f1 100644 --- a/crates/bug/src/application/followers.rs +++ b/crates/bug/src/application/followers.rs @@ -60,7 +60,16 @@ mod tests { async fn seed_bug(repo: &InMemoryBugRepository) -> String { CreateBugUseCase::new(Arc::new(repo.clone())) - .execute("p1", "boom", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "boom", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("seed") .id diff --git a/crates/bug/src/application/list_bugs.rs b/crates/bug/src/application/list_bugs.rs index 44a120e..787fbe8 100644 --- a/crates/bug/src/application/list_bugs.rs +++ b/crates/bug/src/application/list_bugs.rs @@ -36,11 +36,29 @@ mod tests { let repo = Arc::new(InMemoryBugRepository::with_default_flow("p1")); let create = crate::application::CreateBugUseCase::new(repo.clone()); create - .execute("p1", "first", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "first", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("ok"); create - .execute("p1", "second", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "second", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("ok"); @@ -55,7 +73,16 @@ mod tests { async fn isolates_by_project() { let repo = Arc::new(InMemoryBugRepository::with_default_flow("p1")); crate::application::CreateBugUseCase::new(repo.clone()) - .execute("p1", "boom", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "boom", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("ok"); diff --git a/crates/bug/src/application/relations.rs b/crates/bug/src/application/relations.rs index 7e83476..05a7884 100644 --- a/crates/bug/src/application/relations.rs +++ b/crates/bug/src/application/relations.rs @@ -82,7 +82,16 @@ mod tests { async fn seed_bug(repo: &InMemoryBugRepository) -> String { CreateBugUseCase::new(Arc::new(repo.clone())) - .execute("p1", "boom", "NEW", None, None, None, &std::collections::BTreeMap::new()) + .execute( + "p1", + "boom", + "NEW", + None, + None, + None, + None, + &std::collections::BTreeMap::new(), + ) .await .expect("seed") .id diff --git a/crates/bug/src/application/update_meta.rs b/crates/bug/src/application/update_meta.rs index 62977ab..d5b7c9f 100644 --- a/crates/bug/src/application/update_meta.rs +++ b/crates/bug/src/application/update_meta.rs @@ -34,6 +34,7 @@ impl UpdateBugMetaUseCase { title: Option<&str>, severity: Option<&str>, handler: Option<&str>, + description: Option<&str>, operator: Option<&str>, ) -> Result { let severity = normalize_severity(severity)?; @@ -44,7 +45,14 @@ impl UpdateBugMetaUseCase { Some(t) => t.to_string(), }; self.repo - .update_meta(bug_id, &title, severity.as_deref(), handler.as_deref(), operator) + .update_meta( + bug_id, + &title, + severity.as_deref(), + handler.as_deref(), + description, + operator, + ) .await? .ok_or(UpdateBugMetaError::BugNotFound) } @@ -65,6 +73,7 @@ mod tests { Some("alice"), Some("P2"), Some("bob"), + None, &std::collections::BTreeMap::new(), ) .await @@ -78,7 +87,7 @@ mod tests { let uc = UpdateBugMetaUseCase::new(Arc::new(repo)); let bug = uc - .execute(&seeded.id, Some("boom v2"), Some(" p0 "), Some(" carol "), Some("dave")) + .execute(&seeded.id, Some("boom v2"), Some(" p0 "), Some(" carol "), None, Some("dave")) .await .expect("ok"); assert_eq!(bug.title, "boom v2"); @@ -97,7 +106,7 @@ mod tests { let seeded = seed_bug(&repo).await; let uc = UpdateBugMetaUseCase::new(Arc::new(repo)); - let bug = uc.execute(&seeded.id, None, None, None, Some("dave")).await.expect("ok"); + let bug = uc.execute(&seeded.id, None, None, None, None, Some("dave")).await.expect("ok"); assert_eq!(bug.title, "boom"); assert_eq!(bug.severity, None); assert_eq!(bug.handler, None); @@ -110,11 +119,11 @@ mod tests { let uc = UpdateBugMetaUseCase::new(Arc::new(repo)); assert_eq!( - uc.execute(&seeded.id, None, Some("HIGH"), None, None).await.unwrap_err(), + uc.execute(&seeded.id, None, Some("HIGH"), None, None, None).await.unwrap_err(), UpdateBugMetaError::Validation(BugError::InvalidSeverity("HIGH".into())) ); assert_eq!( - uc.execute("ghost", None, None, None, None).await.unwrap_err(), + uc.execute("ghost", None, None, None, None, None).await.unwrap_err(), UpdateBugMetaError::BugNotFound ); } diff --git a/crates/bug/src/domain/bug.rs b/crates/bug/src/domain/bug.rs index 9af9a28..93b3919 100644 --- a/crates/bug/src/domain/bug.rs +++ b/crates/bug/src/domain/bug.rs @@ -89,6 +89,8 @@ pub struct NewBug { pub severity: Option, /// Handler user id. pub handler: Option, + /// Markdown description body (defect details); blank → None. + pub description: Option, /// Validated custom field values (see `normalize_custom_fields`); field definitions /// live in the project template. pub custom_fields: BTreeMap, @@ -106,6 +108,7 @@ impl NewBug { created_by: None, severity: None, handler: None, + description: None, custom_fields: BTreeMap::new(), }) } @@ -126,6 +129,12 @@ impl NewBug { self } + /// Attaches a markdown description; blank text collapses to None. + pub fn with_description(mut self, description: Option) -> Self { + self.description = description.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); + self + } + /// Attaches validated custom fields (caller runs `normalize_custom_fields` first). pub fn with_custom_fields(mut self, custom_fields: BTreeMap) -> Self { self.custom_fields = custom_fields; @@ -146,6 +155,8 @@ pub struct Bug { pub severity: Option, /// Handler user id. pub handler: Option, + /// Markdown description body (defect details). + pub description: Option, /// Last mutator's user id; stamped by every mutation, seeded from created_by on insert. pub updated_by: Option, /// Last mutation time as timestamptz text (adapter-formatted). @@ -198,6 +209,7 @@ mod tests { created_by: None, severity: None, handler: None, + description: None, updated_by: None, updated_at: None, custom_fields: BTreeMap::new(), diff --git a/crates/bug/src/ports/bug_repository.rs b/crates/bug/src/ports/bug_repository.rs index 59b9de5..5d284a2 100644 --- a/crates/bug/src/ports/bug_repository.rs +++ b/crates/bug/src/ports/bug_repository.rs @@ -31,14 +31,16 @@ pub trait BugRepository: Send + Sync { ) -> Result<(), RepoError>; /// Updates title/severity/handler (severity/handler are full replacements, - /// None clears) and stamps updated_by/updated_at. Returns the updated bug, - /// or None when it doesn't exist. + /// None clears) and the markdown description (None keeps the current value, + /// Some sets it, including an empty string to clear). Stamps updated_by/ + /// updated_at. Returns the updated bug, or None when it doesn't exist. async fn update_meta( &self, id: &str, title: &str, severity: Option<&str>, handler: Option<&str>, + description: Option<&str>, operator: Option<&str>, ) -> Result, RepoError>; diff --git a/crates/migrate/migrations/0111_bug_description.sql b/crates/migrate/migrations/0111_bug_description.sql new file mode 100644 index 0000000..af37379 --- /dev/null +++ b/crates/migrate/migrations/0111_bug_description.sql @@ -0,0 +1,2 @@ +-- Bug description: markdown body for defect details (rendered in the bug detail view). +ALTER TABLE ms_bug ADD COLUMN IF NOT EXISTS description TEXT; diff --git a/web/src/api.ts b/web/src/api.ts index aeb4f81..02b897a 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1302,6 +1302,8 @@ export interface Bug { severity?: string | null /** Handler user id. */ handler?: string | null + /** Markdown description body (defect details). */ + description?: string | null /** Last mutator's user id (stamped on every mutation). */ updatedBy?: string | null /** Last mutation time (timestamptz text). */ @@ -1966,9 +1968,9 @@ export const api = { // Bugs — list/create/status transitions all backend-driven (project-scoped, newest first) bugs: (projectId: string) => projectId ? http.get(`/bug?projectId=${encodeURIComponent(projectId)}`) : Promise.resolve([] as Bug[]), - createBug: (b: { projectId: string; title: string; initialStatus: string; severity?: string; handler?: string; customFields?: Record }) => http.post('/bug', b), - // Meta update: severity/handler are full replacements (omit to clear); omitted title keeps the current one. - updateBug: (id: string, b: { title?: string; severity?: string; handler?: string }) => http.put(`/bug/${encodeURIComponent(id)}`, b), + createBug: (b: { projectId: string; title: string; initialStatus: string; severity?: string; handler?: string; description?: string; customFields?: Record }) => http.post('/bug', b), + // Meta update: severity/handler are full replacements (omit to clear); omitted title keeps the current one; omitted description keeps the current value. + updateBug: (id: string, b: { title?: string; severity?: string; handler?: string; description?: string }) => http.put(`/bug/${encodeURIComponent(id)}`, b), setBugStatus: (id: string, status: string) => http.post(`/bug/${id}/status`, { status }), // In-app notifications (message center): always scoped to the current session user. diff --git a/web/src/pages/Bugs.tsx b/web/src/pages/Bugs.tsx index 6e4ae96..1b7b151 100644 --- a/web/src/pages/Bugs.tsx +++ b/web/src/pages/Bugs.tsx @@ -1,11 +1,14 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Button, Empty, Form, Input, Select, Space, Tag, Tooltip } from 'antd' +import { Button, Descriptions, Empty, Form, Input, Select, Space, Tag, Tooltip } from 'antd' import ResizableDrawer from '../components/ResizableDrawer' import { message, modal } from '../feedback' import { LinkOutlined } from '@ant-design/icons' +import { MarkdownEditor } from '../components/MarkdownEditor' +import { MarkdownRenderer } from '../components/MarkdownRenderer' import { api, ApiError, userIdStore, type Bug, type BugRelation } from '../api' import { useApp } from '../context' import { useI18n } from '../i18n' +import { useScopedNavigate } from '../scope' import { SelectProjectEmpty } from '../components/Page' import { Workspace, WorkList, useWorkTabs } from '../components/Workspace' import { useListView, type ListColumn } from '../components/ListView' @@ -15,17 +18,24 @@ import { CF_GROUP, CustomFieldItems, collectCustomValues, useFieldTemplate } fro const STATUSES = ['NEW', 'RESOLVED', 'CLOSED', 'REOPENED', 'REJECTED'] const SEVERITIES = ['P0', 'P1', 'P2', 'P3'] -/** Handler candidates: project members with display names resolved (id fallback). */ +/** Handler candidates: project members first, falling back to (and merged with) the global + * user directory so the assignee list is never empty even for projects with no members. */ function useMemberOptions(projectId: string) { const [options, setOptions] = useState<{ value: string; label: string }[]>([]) useEffect(() => { let alive = true - api - .projectMembers(projectId) - .then(async (ms) => { - const ids = [...new Set(ms.map((m) => m.userId).filter(Boolean))] - const names = ids.length ? await api.userNames(ids).catch(() => ({}) as Record) : {} - if (alive) setOptions(ids.map((id) => ({ value: id, label: names[id] || id }))) + Promise.all([ + api.projectMembers(projectId).catch(() => [] as { userId?: string }[]), + api.users().catch(() => ({ items: [] as { id?: string }[] })), + ]) + .then(async ([ms, us]) => { + const ids = [ + ...ms.map((m) => m.userId).filter(Boolean), + ...us.items.map((u) => u.id).filter(Boolean), + ] as string[] + const uniq = [...new Set(ids)] + const names = uniq.length ? await api.userNames(uniq).catch(() => ({}) as Record) : {} + if (alive) setOptions(uniq.map((id) => ({ value: id, label: names[id] || id }))) }) .catch(() => {}) return () => { alive = false } @@ -127,10 +137,13 @@ function BugsList({ items, loading, projectId, refresh, setItems, relBug, setRel t: (k: string, d?: string) => string }) { const [editBug, setEditBug] = useState(null) + const [detailBug, setDetailBug] = useState(null) + // Jump to the global user directory so a bug's handler can be inspected in /user. + const nav = useScopedNavigate() // Resolve handler/updater ids in the current rows to display names. const [names, setNames] = useState>({}) useEffect(() => { - const ids = [...new Set(items.flatMap((b) => [b.handler, b.updatedBy]).filter((x): x is string => !!x))] + const ids = [...new Set(items.flatMap((b) => [b.createdBy, b.handler, b.updatedBy]).filter((x): x is string => !!x))] if (!ids.length) { setNames({}); return } api.userNames(ids).then(setNames).catch(() => setNames({})) }, [items]) @@ -157,7 +170,9 @@ function BugsList({ items, loading, projectId, refresh, setItems, relBug, setRel label: t('bug.handler', '处理人'), title: t('bug.handler', '处理人'), width: 120, - render: (_, r) => nameOf(r.handler), + render: (_, r) => (r.handler + ? nav('/user')} title={t('bug.openUser', '在用户管理中查看')}>{nameOf(r.handler)} + : '-'), }, { key: 'updatedBy', @@ -262,6 +277,7 @@ function BugsList({ items, loading, projectId, refresh, setItems, relBug, setRel newLabel={t('bug.new', '新建缺陷')} extraActions={lv.toolbar} onRefresh={refresh} + onRowClick={(r) => setDetailBug(r)} columns={lv.columns} data={lv.rows} pagination={lv.pagination} @@ -280,6 +296,7 @@ function BugsList({ items, loading, projectId, refresh, setItems, relBug, setRel setItems((prev) => prev.map((x) => (x.id === b.id ? { ...x, ...b } : x))) }} /> + setDetailBug(null)} /> ) } @@ -288,17 +305,17 @@ function BugsList({ items, loading, projectId, refresh, setItems, relBug, setRel function EditBugDrawer({ bug, projectId, onClose, onSaved }: { bug: Bug | null; projectId: string; onClose: () => void; onSaved: (b: Bug) => void }) { const { t } = useI18n() const members = useMemberOptions(projectId) - const [form] = Form.useForm<{ title: string; severity?: string; handler?: string }>() + const [form] = Form.useForm<{ title: string; severity?: string; handler?: string; description?: string }>() const [saving, setSaving] = useState(false) useEffect(() => { - if (bug) form.setFieldsValue({ title: bug.title || '', severity: bug.severity || undefined, handler: bug.handler || undefined }) + if (bug) form.setFieldsValue({ title: bug.title || '', severity: bug.severity || undefined, handler: bug.handler || undefined, description: bug.description || undefined }) }, [bug, form]) const save = async () => { const v = await form.validateFields() if (!bug) return setSaving(true) try { - const b = await api.updateBug(bug.id, { title: v.title, severity: v.severity, handler: v.handler }) + const b = await api.updateBug(bug.id, { title: v.title, severity: v.severity, handler: v.handler, description: v.description }) message.success(t('bug.updated', '缺陷已更新')) onSaved(b) } catch (e) { @@ -334,11 +351,66 @@ function EditBugDrawer({ bug, projectId, onClose, onSaved }: { bug: Bug | null; + + + + + From c742d9aa536a227a620e9d6e79c448f68e5aa67d Mon Sep 17 00:00:00 2001 From: Liber Date: Tue, 6 Oct 2026 23:34:22 +0800 Subject: [PATCH 4/4] feat: DeepCode executor + Jev verification judge (with cargo-audit dep bumps) (#140) * feat(delivery): DeepCode (DEEPCODE) executor + per-executor run guide - ExecutorKind::DeepCode end to end: domain enum (ALL remains the single authoritative kind list), in-memory + Redis queues, agent-runtime backend (deepcode exec --json, DEEPCODE_CMD override), CLI normalize, MCP dispatch schema, web executor labels - docs/EXECUTORS(.zh-CN).md + USAGE(.zh-CN).md: DeepCode run recipe, env override and backend tables - verified: cargo check/test on delivery + agent-runtime + shepherd-cli, web tsc typecheck; a real deepcode CLI run was not exercised here * chore(deps): upgrade h2 to 0.4.16 and rustls to 0.23.45 per cargo audit - RUSTSEC-2026-0258 (h2 unbounded empty DATA frames): h2 0.4.15 -> 0.4.16 - RUSTSEC-2026-0285 (rustls TLS 1.3 handshake across encryption levels): rustls 0.23.40 -> 0.23.45, rustls-webpki 0.103.13 -> 0.103.15 * feat(server): Jev (TypeSafe System One) verification judge - JevJudge implements Judge: one noul question per acceptance criterion in a single POST to the TypeSafe System One endpoint; passes only when every probability reaches SHEPHERD_JEV_THRESHOLD (default 0.8) - build_judge() priority: Jev (SHEPHERD_JEV_API_KEY, falls back to TYPESAFE_API_KEY) -> LLM judge -> SHEPHERD_JUDGE_URL HTTP -> accept-all; HTTP/parse errors fail closed - docs/USAGE(.zh-CN).md: judge config table + fail-closed note --- Cargo.lock | 34 +-- crates/agent-runtime/src/backend.rs | 5 + crates/agent-runtime/src/main.rs | 1 + crates/delivery/src/adapters/local.rs | 9 +- crates/delivery/src/adapters/queue.rs | 2 +- crates/delivery/src/adapters/redis_queue.rs | 2 +- crates/delivery/src/domain/delivery.rs | 7 +- crates/server/src/jev.rs | 245 ++++++++++++++++++++ crates/server/src/judge.rs | 6 + crates/server/src/main.rs | 1 + crates/server/src/mcp_tools.rs | 4 +- crates/shepherd-cli/src/client.rs | 3 +- crates/shepherd-cli/src/commands/agent.rs | 2 +- crates/shepherd-cli/src/main.rs | 2 + docs/EXECUTORS.md | 27 ++- docs/EXECUTORS.zh-CN.md | 27 ++- docs/USAGE.md | 25 +- docs/USAGE.zh-CN.md | 24 +- web/src/api.ts | 3 +- 19 files changed, 382 insertions(+), 47 deletions(-) create mode 100644 crates/server/src/jev.rs diff --git a/Cargo.lock b/Cargo.lock index 2504aea..e2e2d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -152,7 +152,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1761,7 +1761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2113,9 +2113,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2380,7 +2380,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3103,7 +3103,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3822,7 +3822,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -3860,7 +3860,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -4421,14 +4421,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -4463,9 +4463,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -4909,7 +4909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5445,7 +5445,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6252,7 +6252,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/crates/agent-runtime/src/backend.rs b/crates/agent-runtime/src/backend.rs index 83c313c..86042ef 100644 --- a/crates/agent-runtime/src/backend.rs +++ b/crates/agent-runtime/src/backend.rs @@ -250,6 +250,11 @@ impl GenericCliBackend { timeout, ) } + pub fn deepcode(timeout: Duration) -> Self { + // Headless one-shot; --json keeps the output machine-readable. The prompt + // is appended as the final argv by GenericCliBackend::execute. + Self::from_env("deepcode", "DEEPCODE_CMD", "deepcode exec --json", timeout) + } } #[async_trait] diff --git a/crates/agent-runtime/src/main.rs b/crates/agent-runtime/src/main.rs index b330bf3..cce3d20 100644 --- a/crates/agent-runtime/src/main.rs +++ b/crates/agent-runtime/src/main.rs @@ -117,6 +117,7 @@ fn backend_for(executor: &str, task_timeout: Duration) -> Arc Arc::new(GenericCliBackend::codex(task_timeout)), "OPENCODE" => Arc::new(GenericCliBackend::opencode(task_timeout)), "CODEBUDDY" => Arc::new(GenericCliBackend::codebuddy(task_timeout)), + "DEEPCODE" => Arc::new(GenericCliBackend::deepcode(task_timeout)), _ => Arc::new(ClaudeBackend::new(task_timeout)), } } diff --git a/crates/delivery/src/adapters/local.rs b/crates/delivery/src/adapters/local.rs index 34b46e9..c31af2e 100644 --- a/crates/delivery/src/adapters/local.rs +++ b/crates/delivery/src/adapters/local.rs @@ -36,9 +36,12 @@ impl LocalCommandAgentExecutor { match kind { ExecutorKind::ClaudeCode => &self.claude_code, ExecutorKind::Codex => &self.codex, - // The local path only configures two argvs; OpenCode/CodeBuddy deliberately - // fall back to the claude argv (real routing lives in crates/agent-runtime). - ExecutorKind::OpenCode | ExecutorKind::CodeBuddy => &self.claude_code, + // The local path only configures two argvs; OpenCode/CodeBuddy/DeepCode + // deliberately fall back to the claude argv (real routing lives in + // crates/agent-runtime). + ExecutorKind::OpenCode | ExecutorKind::CodeBuddy | ExecutorKind::DeepCode => { + &self.claude_code + } } } } diff --git a/crates/delivery/src/adapters/queue.rs b/crates/delivery/src/adapters/queue.rs index 63caff4..99370ea 100644 --- a/crates/delivery/src/adapters/queue.rs +++ b/crates/delivery/src/adapters/queue.rs @@ -22,7 +22,7 @@ use crate::ports::{ RuntimeInfo, WorkQueue, WorkSpec, }; -const KNOWN_CAPS: [ExecutorKind; 4] = ExecutorKind::ALL; +const KNOWN_CAPS: [ExecutorKind; 5] = ExecutorKind::ALL; #[derive(Default)] pub struct InMemoryWorkQueue { diff --git a/crates/delivery/src/adapters/redis_queue.rs b/crates/delivery/src/adapters/redis_queue.rs index 86db1d8..42ae4d6 100644 --- a/crates/delivery/src/adapters/redis_queue.rs +++ b/crates/delivery/src/adapters/redis_queue.rs @@ -25,7 +25,7 @@ const ACKMAP: &str = "fleet:ackmap"; const RT_INDEX: &str = "fleet:rt:index"; const SEP: char = '\u{1f}'; -fn known_caps() -> [ExecutorKind; 4] { +fn known_caps() -> [ExecutorKind; 5] { ExecutorKind::ALL } diff --git a/crates/delivery/src/domain/delivery.rs b/crates/delivery/src/domain/delivery.rs index c6b6771..d003ce3 100644 --- a/crates/delivery/src/domain/delivery.rs +++ b/crates/delivery/src/domain/delivery.rs @@ -12,13 +12,14 @@ pub enum ExecutorKind { Codex, OpenCode, CodeBuddy, + DeepCode, } impl ExecutorKind { /// Single authoritative list of executor kinds; queue adapters and tests consume /// it, so a new variant only touches this file. - pub const ALL: [ExecutorKind; 4] = - [Self::ClaudeCode, Self::Codex, Self::OpenCode, Self::CodeBuddy]; + pub const ALL: [ExecutorKind; 5] = + [Self::ClaudeCode, Self::Codex, Self::OpenCode, Self::CodeBuddy, Self::DeepCode]; pub fn as_str(&self) -> &'static str { match self { @@ -26,6 +27,7 @@ impl ExecutorKind { Self::Codex => "CODEX", Self::OpenCode => "OPENCODE", Self::CodeBuddy => "CODEBUDDY", + Self::DeepCode => "DEEPCODE", } } @@ -35,6 +37,7 @@ impl ExecutorKind { "CODEX" => Some(Self::Codex), "OPENCODE" => Some(Self::OpenCode), "CODEBUDDY" => Some(Self::CodeBuddy), + "DEEPCODE" => Some(Self::DeepCode), _ => None, } } diff --git a/crates/server/src/jev.rs b/crates/server/src/jev.rs new file mode 100644 index 0000000..a0afb9b --- /dev/null +++ b/crates/server/src/jev.rs @@ -0,0 +1,245 @@ +//! Jev (TypeSafe System One) verification judge: a fast typed-judgment model. +//! +//! Jev answers `noul` (yes/no probability) questions about a blob of state — +//! no prose, no code generation. That makes it a drop-in, cheap alternative to +//! the LLM judge for the delivery verification gate: one `noul` per acceptance +//! criterion, all in a single HTTP call; the gate passes only when every +//! probability reaches the configured threshold (default 0.8). +//! +//! Env vars: `SHEPHERD_JEV_API_KEY` (falls back to `TYPESAFE_API_KEY`), +//! `SHEPHERD_JEV_URL` (default `https://api.typesafe.ai/v1/systemone`), +//! `SHEPHERD_JEV_MODEL` (default `jev-latest`), +//! `SHEPHERD_JEV_THRESHOLD` (default `0.8`). + +use std::collections::BTreeMap; +use std::time::Duration; + +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; + +use orchestrator::ports::{DeliverableView, Judge, Verdict}; + +const DEFAULT_URL: &str = "https://api.typesafe.ai/v1/systemone"; +const DEFAULT_MODEL: &str = "jev-latest"; +const DEFAULT_THRESHOLD: f64 = 0.8; + +#[derive(Deserialize)] +struct SystemOneResponse { + #[serde(default)] + answers: BTreeMap, +} + +#[derive(Deserialize)] +struct NoulAnswer { + noul: f64, +} + +pub struct JevJudge { + client: reqwest::Client, + url: String, + key: String, + model: String, + threshold: f64, +} + +impl JevJudge { + pub fn from_env() -> Option { + let key = std::env::var("SHEPHERD_JEV_API_KEY") + .ok() + .filter(|s| !s.trim().is_empty()) + .or_else(|| std::env::var("TYPESAFE_API_KEY").ok().filter(|s| !s.trim().is_empty()))?; + let url = env_or("SHEPHERD_JEV_URL", DEFAULT_URL); + let model = env_or("SHEPHERD_JEV_MODEL", DEFAULT_MODEL); + let threshold = std::env::var("SHEPHERD_JEV_THRESHOLD") + .ok() + .and_then(|v| v.parse().ok()) + .filter(|t| (0.0..=1.0).contains(t)) + .unwrap_or(DEFAULT_THRESHOLD); + let client = reqwest::Client::builder() + .no_proxy() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Some(Self { client, url, key, model, threshold }) + } + + #[cfg(test)] + fn new(url: impl Into, key: impl Into, threshold: f64) -> Self { + Self { + client: reqwest::Client::new(), + url: url.into(), + key: key.into(), + model: DEFAULT_MODEL.into(), + threshold, + } + } + + fn state(criteria: &[String], deliverable: &DeliverableView) -> String { + let criteria = criteria.iter().map(|c| format!("- {c}")).collect::>().join("\n"); + format!( + "Acceptance criteria:\n{criteria}\n\nDeliverable:\nkind: {}\nreference: {}\nsummary: {}", + deliverable.kind, deliverable.reference, deliverable.summary + ) + } +} + +#[async_trait] +impl Judge for JevJudge { + async fn judge(&self, criteria: &[String], deliverable: &DeliverableView) -> Verdict { + if criteria.is_empty() { + return Verdict { passed: true, reason: "jev: no criteria to judge".into() }; + } + let questions: BTreeMap = criteria + .iter() + .enumerate() + .map(|(i, c)| { + ( + format!("c{i}"), + json!({ + "type": "noul", + "instructions": format!("Does the deliverable satisfy this acceptance criterion: {c}"), + }), + ) + }) + .collect(); + let body = json!({ "state": Self::state(criteria, deliverable), "model": self.model, "questions": questions }); + + let resp = match self.client.post(&self.url).bearer_auth(&self.key).json(&body).send().await + { + Ok(r) if r.status().is_success() => r, + Ok(r) => return Verdict { passed: false, reason: format!("jev HTTP {}", r.status()) }, + Err(e) => return Verdict { passed: false, reason: format!("jev unreachable: {e}") }, + }; + let parsed: SystemOneResponse = match resp.json().await { + Ok(p) => p, + Err(e) => { + return Verdict { + passed: false, + reason: format!("failed to parse jev response: {e}"), + } + } + }; + + // Every criterion must clear the threshold; report the weakest one. + let mut weakest = (1.0_f64, String::new()); + for (i, c) in criteria.iter().enumerate() { + let key = format!("c{i}"); + let p = match parsed.answers.get(&key) { + Some(a) => a.noul, + None => { + return Verdict { + passed: false, + reason: format!("jev response missing answer {key}"), + } + } + }; + if p < weakest.0 { + weakest = (p, c.clone()); + } + } + if weakest.0 >= self.threshold { + Verdict { + passed: true, + reason: format!( + "jev: min noul {:.2} >= threshold {:.2}", + weakest.0, self.threshold + ), + } + } else { + Verdict { + passed: false, + reason: format!( + "jev: noul {:.2} < threshold {:.2} for criterion: {}", + weakest.0, self.threshold, weakest.1 + ), + } + } + } +} + +fn env_or(key: &str, default: &str) -> String { + std::env::var(key).ok().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| default.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{routing::post, Json, Router}; + + async fn spawn(app: Router, path: &str) -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let addr = listener.local_addr().expect("addr"); + tokio::spawn(async move { axum::serve(listener, app).await.expect("serve") }); + format!("http://{addr}{path}") + } + + fn dv() -> DeliverableView { + DeliverableView { + kind: "DIFF".into(), + reference: "branch:feat".into(), + summary: "done".into(), + } + } + + #[tokio::test] + async fn jev_judge_passes_when_every_criterion_clears_threshold() { + let app = Router::new().route( + "/v1/systemone", + post(|| async { + Json(json!({ + "model": "jev-1.13.0", + "answers": { "c0": { "type": "noul", "noul": 0.95 }, "c1": { "type": "noul", "noul": 0.9 } }, + "usage": { "input_tokens": 1, "output_tokens": 1 } + })) + }), + ); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into(), "token issued".into()], &dv()).await; + assert!(v.passed, "got: {v:?}"); + assert!(v.reason.contains("0.90"), "reason names the weakest: {}", v.reason); + } + + #[tokio::test] + async fn jev_judge_fails_on_criterion_below_threshold() { + let app = Router::new().route( + "/v1/systemone", + post(|| async { + Json(json!({ + "model": "jev-1.13.0", + "answers": { "c0": { "type": "noul", "noul": 0.99 }, "c1": { "type": "noul", "noul": 0.5 } }, + "usage": { "input_tokens": 1, "output_tokens": 1 } + })) + }), + ); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into(), "token issued".into()], &dv()).await; + assert!(!v.passed); + assert!( + v.reason.contains("token issued"), + "reason names the failing criterion: {}", + v.reason + ); + } + + #[tokio::test] + async fn jev_judge_passes_with_no_criteria() { + let j = JevJudge::new("http://127.0.0.1:9", "k", 0.8); + let v = j.judge(&[], &dv()).await; + assert!(v.passed); + assert_eq!(v.reason, "jev: no criteria to judge"); + } + + #[tokio::test] + async fn jev_judge_fail_closed_on_bad_json() { + let app = Router::new().route("/v1/systemone", post(|| async { "not json at all" })); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into()], &dv()).await; + assert!(!v.passed); + assert!(v.reason.contains("parse"), "got: {}", v.reason); + } +} diff --git a/crates/server/src/judge.rs b/crates/server/src/judge.rs index dec3253..ec8174d 100644 --- a/crates/server/src/judge.rs +++ b/crates/server/src/judge.rs @@ -41,6 +41,12 @@ impl Judge for HttpJudge { } pub fn build_judge() -> Arc { + // Jev first: it is the cheap typed-judgment alternative to the LLM judge. + // Setting SHEPHERD_JEV_API_KEY (or TYPESAFE_API_KEY) routes the verification + // gate to Jev even when SHEPHERD_LLM_URL is also set for planner/PRD use. + if let Some(j) = crate::jev::JevJudge::from_env() { + return Arc::new(j); + } if let Some(j) = crate::llm::judge() { return j; } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 9575e27..0ab2bc0 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -11,6 +11,7 @@ mod debug_send; mod decomposition_run; mod design_bridge; mod import_scheduler; +mod jev; mod judge; mod llm; mod mcp_bus; diff --git a/crates/server/src/mcp_tools.rs b/crates/server/src/mcp_tools.rs index 17902ee..251efa2 100644 --- a/crates/server/src/mcp_tools.rs +++ b/crates/server/src/mcp_tools.rs @@ -485,7 +485,7 @@ pub fn router( .requires("TASK", "ADD")) .tool(Tool::new( "shepherd_dispatch_delivery", - "把任务派发给 AI 执行者(executor: CLAUDE_CODE | CODEX | OPENCODE | CODEBUDDY);自动驱动任务、过验证门并回灌验证。\ + "把任务派发给 AI 执行者(executor: CLAUDE_CODE | CODEX | OPENCODE | CODEBUDDY | DEEPCODE);自动驱动任务、过验证门并回灌验证。\ 可直接传 skillIds(+projectId)自动组合行为规范注入,无需先调 compose。", obj( json!({ @@ -494,7 +494,7 @@ pub fn router( "title": { "type": "string" }, "description": { "type": "string" }, "acceptanceCriteria": { "type": "array", "items": { "type": "string" } }, - "executor": { "type": "string", "enum": ["CLAUDE_CODE", "CODEX", "OPENCODE", "CODEBUDDY"] }, + "executor": { "type": "string", "enum": ["CLAUDE_CODE", "CODEX", "OPENCODE", "CODEBUDDY", "DEEPCODE"] }, "projectId": { "type": "string", "description": "skillIds 非空时必填(用于组合)" }, "skillIds": { "type": "array", "items": { "type": "string" }, "description": "可选:自动组合这些 skill 为行为规范" }, "instructions": { "type": "string", "description": "可选:显式行为规范(与 skillIds 组合结果合并)" } diff --git a/crates/shepherd-cli/src/client.rs b/crates/shepherd-cli/src/client.rs index 14e0968..26f5d44 100644 --- a/crates/shepherd-cli/src/client.rs +++ b/crates/shepherd-cli/src/client.rs @@ -345,8 +345,9 @@ pub fn normalize_agent(t: &str) -> R { "opencode" => Ok("OPENCODE".into()), // The brand is one word, but the claude-code spelling invites code-buddy; accept both. "codebuddy" | "code_buddy" => Ok("CODEBUDDY".into()), + "deepcode" | "deep_code" => Ok("DEEPCODE".into()), other => Err(format!( - "unknown agent type: {other} (supported: claude-code | codex | opencode | codebuddy)" + "unknown agent type: {other} (supported: claude-code | codex | opencode | codebuddy | deepcode)" ) .into()), } diff --git a/crates/shepherd-cli/src/commands/agent.rs b/crates/shepherd-cli/src/commands/agent.rs index 2c11cdf..fb12cad 100644 --- a/crates/shepherd-cli/src/commands/agent.rs +++ b/crates/shepherd-cli/src/commands/agent.rs @@ -3,7 +3,7 @@ use clap::Subcommand; #[derive(Subcommand)] pub enum AgentCmd { - /// Connect an AI executor (claude-code | codex | opencode | codebuddy) and save it as the dispatch default. + /// Connect an AI executor (claude-code | codex | opencode | codebuddy | deepcode) and save it as the dispatch default. Connect { #[arg(long = "type")] kind: String, diff --git a/crates/shepherd-cli/src/main.rs b/crates/shepherd-cli/src/main.rs index 816265a..a2b4dba 100644 --- a/crates/shepherd-cli/src/main.rs +++ b/crates/shepherd-cli/src/main.rs @@ -323,6 +323,8 @@ mod tests { assert_eq!(normalize_agent("opencode").unwrap(), "OPENCODE"); assert_eq!(normalize_agent("CodeBuddy").unwrap(), "CODEBUDDY"); assert_eq!(normalize_agent("code-buddy").unwrap(), "CODEBUDDY"); + assert_eq!(normalize_agent("deepcode").unwrap(), "DEEPCODE"); + assert_eq!(normalize_agent("DeepCode").unwrap(), "DEEPCODE"); assert!(normalize_agent("gpt").is_err()); } diff --git a/docs/EXECUTORS.md b/docs/EXECUTORS.md index bbaa562..efaae00 100644 --- a/docs/EXECUTORS.md +++ b/docs/EXECUTORS.md @@ -1,6 +1,6 @@ # Shepherd — Running AI Executors -How to run each supported AI executor (Claude Code / Codex / OpenCode / CodeBuddy) +How to run each supported AI executor (Claude Code / Codex / OpenCode / CodeBuddy / DeepCode) 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). @@ -10,6 +10,7 @@ behind `agent-runtime`. For fleet architecture and server-side setup see - [Codex](#codex) - [OpenCode](#opencode) - [CodeBuddy](#codebuddy) +- [DeepCode](#deepcode) - [One runtime, multiple executors](#one-runtime-multiple-executors) - [Mock executor (no CLI)](#mock-executor-no-cli) - [Windows](#windows) @@ -24,7 +25,7 @@ behind `agent-runtime`. For fleet architecture and server-side setup see ## How dispatch reaches a CLI Every delivery attempt carries an `executor` kind (`CLAUDE_CODE` / `CODEX` / -`OPENCODE` / `CODEBUDDY`). The server enqueues the task per kind; an +`OPENCODE` / `CODEBUDDY` / `DEEPCODE`). The server enqueues the task per kind; an `agent-runtime` long-polls with its `SHEPHERD_CAPS` and only claims kinds it declared. On claim, the runtime spawns the matching CLI in a dedicated git worktree, snapshots the resulting changes as a commit, and reports back. @@ -148,6 +149,20 @@ SHEPHERD_CAPS=CODEBUDDY AGENT_WORKDIR=/repo … ./agent-runtime Override with `CODEBUDDY_CMD`, e.g. widen permissions for tasks that need to run shell commands: `CODEBUDDY_CMD="codebuddy -p --permission-mode bypassPermissions"`. +## DeepCode + +Generic backend. The default invocation is `deepcode exec --json ""` — +`--json` keeps the output machine-readable for the result summary. + +```bash +deepcode --version + +SHEPHERD_CAPS=DEEPCODE AGENT_WORKDIR=/repo … ./agent-runtime +``` + +Override with `DEEPCODE_CMD`, e.g. widen permissions for tasks that need to +edit files headlessly: `DEEPCODE_CMD="deepcode exec --access full-access --json"`. + ## One runtime, multiple executors A single runtime can claim several kinds — list them all and make sure every @@ -169,7 +184,7 @@ canned output without spawning a CLI — useful to smoke-test the dispatch loop before installing real CLIs. ```bash -AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY … ./agent-runtime +AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY,DEEPCODE … ./agent-runtime ``` ## Windows @@ -183,7 +198,7 @@ The runtime runs natively on Windows (build with the MSVC toolchain: `codex.exe` take precedence); explicit paths via `CLAUDE_BIN` / `*_CMD` are used as-is. `CLAUDE_BIN=D:\nvm4w\nodejs\claude.cmd` works: the Claude backend feeds the prompt over stdin, so the cmd.exe hop is harmless. -- **Generic backends (codebuddy/codex/opencode) cannot go through a `.cmd` +- **Generic backends (codebuddy/codex/opencode/deepcode) cannot go through a `.cmd` shim**: they pass the prompt as an argv, prompts contain newlines, and Rust refuses newline arguments to batch files (cmd.exe cannot carry them safely). Bypass cmd.exe by invoking node directly — read the shim (`type @@ -254,7 +269,7 @@ shepherd dispatch --decomp d1 --task t1 --executor CODEBUDDY # or bind a default once: shepherd agent connect --kind codebuddy ``` -The MCP tool `shepherd_dispatch_delivery` accepts the same four kinds. +The MCP tool `shepherd_dispatch_delivery` accepts the same five kinds. ### Pinning a task to one runtime @@ -323,5 +338,5 @@ animation look the same as local ones. |---|---| | Task stays queued (`ready` grows) | No online runtime declares that kind — compare `SHEPHERD_CAPS` with the task's `executor`, then `GET /agent/runtime` for liveness | | Attempt delivers with "no code change" | CLI ran but refused edits (permission mode) or the prompt didn't ask for file changes — read the deliverable summary, it contains the CLI's own output | -| Spawn error in events | CLI not on `PATH` inside the runtime's environment — set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` | +| Spawn error in events | CLI not on `PATH` inside the runtime's environment — set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD` | | Attempt fails at timeout | Raise `AGENT_TASK_TIMEOUT_SECS`; generic backends get killed (process group) at the deadline | diff --git a/docs/EXECUTORS.zh-CN.md b/docs/EXECUTORS.zh-CN.md index 728862f..5712220 100644 --- a/docs/EXECUTORS.zh-CN.md +++ b/docs/EXECUTORS.zh-CN.md @@ -1,7 +1,7 @@ # Shepherd — AI 执行者运行指南 介绍如何在 `agent-runtime` 下运行各家 AI 执行者(Claude Code / Codex / OpenCode / -CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-CN.md), +CodeBuddy / DeepCode)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-CN.md), 镜像构建与部署见 [DEPLOYMENT.zh-CN.md](./DEPLOYMENT.zh-CN.md)。 - [派发如何到达 CLI](#派发如何到达-cli) @@ -10,6 +10,7 @@ CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-C - [Codex](#codex) - [OpenCode](#opencode) - [CodeBuddy](#codebuddy) +- [DeepCode](#deepcode) - [一个 runtime 承接多种执行者](#一个-runtime-承接多种执行者) - [mock 执行者(无需 CLI)](#mock-执行者无需-cli) - [Windows](#windows) @@ -24,7 +25,7 @@ CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-C ## 派发如何到达 CLI 每个交付尝试都带 `executor` 类型(`CLAUDE_CODE` / `CODEX` / `OPENCODE` / -`CODEBUDDY`)。服务端按类型入队;`agent-runtime` 以 `SHEPHERD_CAPS` 长轮询, +`CODEBUDDY` / `DEEPCODE`)。服务端按类型入队;`agent-runtime` 以 `SHEPHERD_CAPS` 长轮询, 只认领自己声明过的类型。认领后 runtime 在独立 git worktree 中拉起对应 CLI, 把产生的改动快照成 commit 并回报。 @@ -140,6 +141,20 @@ SHEPHERD_CAPS=CODEBUDDY AGENT_WORKDIR=/repo … ./agent-runtime 用 `CODEBUDDY_CMD` 覆盖,例如任务需要跑 shell 时放宽权限: `CODEBUDDY_CMD="codebuddy -p --permission-mode bypassPermissions"`。 +## DeepCode + +通用后端。默认调用 `deepcode exec --json ""` —— `--json` 让输出保持 +机器可读,便于回填结果摘要。 + +```bash +deepcode --version + +SHEPHERD_CAPS=DEEPCODE AGENT_WORKDIR=/repo … ./agent-runtime +``` + +用 `DEEPCODE_CMD` 覆盖,例如 headless 下需要放开文件编辑权限时: +`DEEPCODE_CMD="deepcode exec --access full-access --json"`。 + ## 一个 runtime 承接多种执行者 单个 runtime 可以声明多种类型——全部列出,并保证对应 CLI 都装好且已登录: @@ -158,7 +173,7 @@ runtime(能力任意组合)。服务端设了 `SHEPHERD_FLEET_REDIS` 时,跨机 CLI——适合在装 CLI 之前先冒烟验证派发链路。 ```bash -AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY … ./agent-runtime +AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY,DEEPCODE … ./agent-runtime ``` ## Windows @@ -171,7 +186,7 @@ runtime 可在 Windows 原生运行(MSVC 工具链:`cargo build --release -p age (`codex.exe` 这类原生二进制优先);`CLAUDE_BIN` / `*_CMD` 里写的显式路径原样使用。 `CLAUDE_BIN=D:\nvm4w\nodejs\claude.cmd` 没问题:Claude 后端的 prompt 走 stdin, 经 cmd.exe 一跳无害。 -- **通用后端(codebuddy/codex/opencode)不能走 `.cmd` 垫片**:它们把 prompt 作为 +- **通用后端(codebuddy/codex/opencode/deepcode)不能走 `.cmd` 垫片**:它们把 prompt 作为 命令行参数传,prompt 必含换行,而 Rust 拒绝给批处理文件传含换行的参数 (cmd.exe 无法安全携带)。绕开 cmd.exe,直接用 node 调包入口——先 `type codebuddy.cmd` 看垫片真正调用什么,然后照抄,如 @@ -233,7 +248,7 @@ shepherd dispatch --decomp d1 --task t1 --executor CODEBUDDY # 或先绑定默认:shepherd agent connect --kind codebuddy ``` -MCP 工具 `shepherd_dispatch_delivery` 同样接受这四种类型。 +MCP 工具 `shepherd_dispatch_delivery` 同样接受这五种类型。 ### 定向到具体某台 runtime @@ -293,5 +308,5 @@ cargo r -p pool-runner |---|---| | 任务一直排队(`ready` 增长) | 没有在线 runtime 声明该类型——核对 `SHEPHERD_CAPS` 与任务 `executor`,再看 `GET /agent/runtime` 的在线状态 | | 交付显示"无代码变动" | CLI 跑了但拒绝改文件(权限模式),或提示词本身没要求改文件——看交付摘要,里面是 CLI 的原始输出 | -| 事件里报 spawn 错误 | CLI 不在 runtime 进程的 `PATH` 上——设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` | +| 事件里报 spawn 错误 | CLI 不在 runtime 进程的 `PATH` 上——设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD` | | 任务超时失败 | 调大 `AGENT_TASK_TIMEOUT_SECS`;通用后端到点会被整进程组杀掉 | diff --git a/docs/USAGE.md b/docs/USAGE.md index 610b285..86f9f7a 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -20,7 +20,7 @@ you file a requirement │ │ dispatch ▼ ▼ FLEET DISPATCH ──► agent-runtime executor - (pull / long-poll) (claude / codex / opencode / codebuddy) + (pull / long-poll) (claude / codex / opencode / codebuddy / deepcode) │ runs in a git worktree ▼ deliverable (diff / PR) @@ -59,7 +59,7 @@ Company AI tools (Claude Code, Codex, …) run on internal dev machines or CI | Quick start (Docker) | Docker + Docker Compose v2 | | From-source dev | Rust (stable, edition 2021; CI uses `rust:1.86`), Node.js 18+, a PostgreSQL 16 instance | | Multi-host fleet | Redis 7 | -| Real AI executors | `git` plus the agent CLIs on `PATH` (`claude` / `codex` / `opencode` / `codebuddy`) | +| Real AI executors | `git` plus the agent CLIs on `PATH` (`claude` / `codex` / `opencode` / `codebuddy` / `deepcode`) | PostgreSQL is required; the server **auto-applies migrations on startup**. Redis is required **only** for the multi-host fleet. @@ -187,6 +187,23 @@ All server start-up switches are consolidated into a typed config (`crates/serve Advanced/lazy-read switches also exist for the pluggable AI touchpoints — `SHEPHERD_AGENT_URL` / `SHEPHERD_AGENT_CMD` / `SHEPHERD_AGENT_ASYNC` (executor routing), `SHEPHERD_LLM_URL`, `SHEPHERD_PLANNER_URL`, `SHEPHERD_JUDGE_URL`, `SHEPHERD_MAX_REVISIONS`. Defaults need none of them. +#### 5.1.1 Verification judge: Jev + +The delivery verification gate (`Judge`) can run on [Jev](https://learnjev.com), TypeSafe's +fast typed-judgment model (yes/no probability answers, no prose) instead of an LLM. +Set the key and the gate asks one `noul` question per acceptance criterion in a single +call; it passes only when every probability reaches the threshold. + +| Variable | Default | Meaning | +|---|---|---| +| `SHEPHERD_JEV_API_KEY` | *(falls back to `TYPESAFE_API_KEY`)* | TypeSafe API key — presence enables the Jev judge (takes priority over the LLM judge) | +| `SHEPHERD_JEV_URL` | `https://api.typesafe.ai/v1/systemone` | Jev System One endpoint | +| `SHEPHERD_JEV_MODEL` | `jev-latest` | Jev model alias | +| `SHEPHERD_JEV_THRESHOLD` | `0.8` | Minimum `noul` probability per criterion to pass | + +Failures fail closed: any HTTP/parse error returns a not-passed verdict, so a broken +Jev setup blocks delivery instead of silently passing it. + ### 5.2 Agent-runtime (`agent-runtime`) | Variable | Default | Meaning | @@ -203,6 +220,7 @@ Advanced/lazy-read switches also exist for the pluggable AI touchpoints — `SHE | `CODEX_CMD` | `codex exec` | Codex CLI invocation | | `OPENCODE_CMD` | `opencode run` | OpenCode CLI invocation | | `CODEBUDDY_CMD` | `codebuddy -p --permission-mode acceptEdits` | CodeBuddy CLI invocation | +| `DEEPCODE_CMD` | `deepcode exec --json` | DeepCode CLI invocation | --- @@ -275,6 +293,7 @@ The runtime picks a backend per task by its `executor` kind, unless `AGENT_MOCK= | `CODEX` | generic CLI | `codex exec` (`CODEX_CMD`) | | `OPENCODE` | generic CLI | `opencode run` (`OPENCODE_CMD`) | | `CODEBUDDY` | generic CLI | `codebuddy -p --permission-mode acceptEdits` (`CODEBUDDY_CMD`) | +| `DEEPCODE` | generic CLI | `deepcode exec --json` (`DEEPCODE_CMD`) | | any (with `AGENT_MOCK=1`) | mock — returns canned output | none | Real backends need `git` and the CLI on `PATH` (or pointed at via the override env). Per-executor run recipes (login, permission modes, dispatch examples) are in [EXECUTORS.md](./EXECUTORS.md). Adding a new backend means implementing one `CliAgentBackend` (`async fn execute(prompt, cwd, sink)`) and registering an enum variant — see `crates/agent-runtime/src/backend.rs`. @@ -344,7 +363,7 @@ Both honour `SHEPHERD_BASE` (default `http://127.0.0.1:9180`) and `SHEPHERD_USER | Web console shows blank / API 404 in dev | Vite proxy target mismatch. The dev proxy points at `:9180`; bind the server there or set `SHEPHERD_API` to your server URL. | | Tasks never get claimed | Server not in fleet mode (`SHEPHERD_AGENT_FLEET=1`), no runtime online, or capability mismatch — check `SHEPHERD_CAPS` vs the task's executor kind, and `GET /agent/work/stats`. | | Multi-host runtimes can't share work | `SHEPHERD_FLEET_REDIS` not set (or not the same Redis) on all server replicas → each falls back to its own in-process queue. | -| Real agent does nothing / errors spawning | CLI not on `PATH`; set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD`, or run with `AGENT_MOCK=1` to confirm the loop. | +| Real agent does nothing / errors spawning | CLI not on `PATH`; set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD`, or run with `AGENT_MOCK=1` to confirm the loop. | | API batch-run stuck `RUNNING` with no results | `SHEPHERD_RUNNER=noop` is set (demo placeholder). Unset it to use the native runner. | | New migration not applied | Restart the server — migrations run on boot; a new migration file needs a rebuild. | | OIDC endpoint 404 | The provider is only registered when **both** id and secret env vars are set. | diff --git a/docs/USAGE.zh-CN.md b/docs/USAGE.zh-CN.md index a8b242e..d3adead 100644 --- a/docs/USAGE.zh-CN.md +++ b/docs/USAGE.zh-CN.md @@ -20,7 +20,7 @@ Shepherd 是一个 AI 研发**监督**平台:从 idea 到交付,更安全、更 │ │ 派发 ▼ ▼ 机群派发 ──► agent-runtime 执行者 - (拉取 / 长轮询) (claude / codex / opencode / codebuddy) + (拉取 / 长轮询) (claude / codex / opencode / codebuddy / deepcode) │ 在独立 git worktree 中运行 ▼ 交付物(diff / PR) @@ -59,7 +59,7 @@ Shepherd 是一个 AI 研发**监督**平台:从 idea 到交付,更安全、更 | 快速开始(Docker) | Docker + Docker Compose v2 | | 源码开发 | Rust(stable,edition 2021;CI 用 `rust:1.86`)、Node.js 18+、一个 PostgreSQL 16 实例 | | 多机机群 | Redis 7 | -| 真实 AI 执行者 | `git` + agent CLI 在 `PATH` 上(`claude` / `codex` / `opencode` / `codebuddy`) | +| 真实 AI 执行者 | `git` + agent CLI 在 `PATH` 上(`claude` / `codex` / `opencode` / `codebuddy` / `deepcode`) | PostgreSQL 必需;服务端**启动时自动跑迁移**。Redis 仅在多机机群时必需。 @@ -186,6 +186,22 @@ cargo clippy --workspace -- -D warnings 可插拔 AI 触点还有若干进阶/懒读开关:`SHEPHERD_AGENT_URL` / `SHEPHERD_AGENT_CMD` / `SHEPHERD_AGENT_ASYNC`(执行者路由)、`SHEPHERD_LLM_URL`、`SHEPHERD_PLANNER_URL`、`SHEPHERD_JUDGE_URL`、`SHEPHERD_MAX_REVISIONS`。默认全都无需设置。 +#### 5.1.1 验证门 Judge:Jev + +交付验证门(`Judge`)可以跑在 [Jev](https://learnjev.com)(TypeSafe 的快速类型化判断 +模型,只回答是/否概率,不生成文字)上,替代 LLM judge。配好 key 后,验证门会为每条 +验收标准发一个 `noul` 问题(单次请求),所有概率都达到阈值才判过。 + +| 变量 | 默认 | 含义 | +|---|---|---| +| `SHEPHERD_JEV_API_KEY` | *(缺省回退 `TYPESAFE_API_KEY`)* | TypeSafe API key——设了即启用 Jev judge(优先级高于 LLM judge) | +| `SHEPHERD_JEV_URL` | `https://api.typesafe.ai/v1/systemone` | Jev System One 端点 | +| `SHEPHERD_JEV_MODEL` | `jev-latest` | Jev 模型别名 | +| `SHEPHERD_JEV_THRESHOLD` | `0.8` | 每条标准的最低 `noul` 通过概率 | + +失败一律 fail-closed:任何 HTTP/解析错误都判为不通过,即 Jev 配置坏了会阻断交付, +而不是静默放行。 + ### 5.2 Agent-runtime(`agent-runtime`) | 变量 | 默认 | 含义 | @@ -202,6 +218,7 @@ cargo clippy --workspace -- -D warnings | `CODEX_CMD` | `codex exec` | Codex CLI 调用 | | `OPENCODE_CMD` | `opencode run` | OpenCode CLI 调用 | | `CODEBUDDY_CMD` | `codebuddy -p --permission-mode acceptEdits` | CodeBuddy CLI 调用 | +| `DEEPCODE_CMD` | `deepcode exec --json` | DeepCode CLI 调用 | --- @@ -271,6 +288,7 @@ runtime 按任务的 `executor` 类型选后端,除非 `AGENT_MOCK=1` 强制走 | `CODEX` | 通用 CLI | `codex exec`(`CODEX_CMD`) | | `OPENCODE` | 通用 CLI | `opencode run`(`OPENCODE_CMD`) | | `CODEBUDDY` | 通用 CLI | `codebuddy -p --permission-mode acceptEdits`(`CODEBUDDY_CMD`) | +| `DEEPCODE` | 通用 CLI | `deepcode exec --json`(`DEEPCODE_CMD`) | | 任意(配 `AGENT_MOCK=1`) | mock——返回固定输出 | 无 | 真实后端需 `git` 与 CLI 在 `PATH`(或经覆盖 env 指定)。各执行者的运行指南(登录、权限模式、派发示例)见 [EXECUTORS.zh-CN.md](./EXECUTORS.zh-CN.md)。新增后端只需实现一个 `CliAgentBackend`(`async fn execute(prompt, cwd, sink)`)并注册一个枚举变体——见 `crates/agent-runtime/src/backend.rs`。 @@ -340,7 +358,7 @@ python3 .agents/skills/openapi-bootstrap/scenarios_all.py | dev 下控制台空白 / API 404 | Vite 代理目标不匹配。dev 代理指向 `:9180`;把服务端绑到那里,或设 `SHEPHERD_API` 为你的服务端 URL。 | | 任务始终无人认领 | 服务端未开机群模式(`SHEPHERD_AGENT_FLEET=1`)、无 runtime 在线,或能力不匹配——核对 `SHEPHERD_CAPS` 与任务 executor 类型,以及 `GET /agent/work/stats`。 | | 多机 runtime 无法共享任务 | 各服务端副本未设(或非同一个)`SHEPHERD_FLEET_REDIS` → 各自回落到自己的进程内队列。 | -| 真实 agent 不动作 / spawn 报错 | CLI 不在 `PATH`;设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD`,或先用 `AGENT_MOCK=1` 验证链路。 | +| 真实 agent 不动作 / spawn 报错 | CLI 不在 `PATH`;设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD`,或先用 `AGENT_MOCK=1` 验证链路。 | | 接口批量运行卡在 `RUNNING` 无结果 | 设了 `SHEPHERD_RUNNER=noop`(演示占位)。取消它以走原生 runner。 | | 新迁移未生效 | 重启服务端——迁移在启动时跑;新迁移文件需重新构建。 | | OIDC 端点 404 | provider 仅在 id 与 secret 两个 env **都**设置时才注册。 | diff --git a/web/src/api.ts b/web/src/api.ts index 02b897a..8e74e8f 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1154,6 +1154,7 @@ export const EXECUTOR_LABEL: Record = { CODEX: 'Codex', OPENCODE: 'OpenCode', CODEBUDDY: 'CodeBuddy', + DEEPCODE: 'DeepCode', } /** One task-center row: system-wide delivery attempts aggregated by status/executor/result/completion rate. */ @@ -1166,7 +1167,7 @@ export interface TaskCenterItem { description: string /** Module (title of the owning requirement; empty string when none). */ module: string - /** Executor: CLAUDE_CODE / CODEX / OPENCODE / CODEBUDDY. */ + /** Executor: CLAUDE_CODE / CODEX / OPENCODE / CODEBUDDY / DEEPCODE. */ executor: string /** Target runtime name for directed dispatch; empty = any runtime with the capability. */ targetRuntime?: string | null