fix(glossary): 补齐术语删除和审核门禁
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-08 20:59:27 +08:00
parent 94483ff14d
commit 0275a890bc
19 changed files with 589 additions and 32 deletions
+28 -3
View File
@@ -74,9 +74,9 @@ mod translation_query;
#[path = "workflow_commands.rs"]
mod workflow_commands;
use glossary_query::{
glossary_diagnose_rpc_report, glossary_mutation_rpc_report, glossary_query_rpc_report,
glossary_review_rpc_report, glossary_rpc_envelope, glossary_summary_rpc_report,
run_glossary_command,
glossary_delete_rpc_report, glossary_diagnose_rpc_report, glossary_mutation_rpc_report,
glossary_query_rpc_report, glossary_review_rpc_report, glossary_rpc_envelope,
glossary_summary_rpc_report, run_glossary_command,
};
use patch_commands::{
is_write_patch_command, run_write_patch_command, validate_write_patch_options,
@@ -243,6 +243,7 @@ fn run() -> anyhow::Result<i32> {
| CliCommand::GlossaryUpdate
| CliCommand::GlossaryApprove
| CliCommand::GlossaryDeprecate
| CliCommand::GlossaryDelete
| CliCommand::GlossaryDiagnose => {
run_glossary_command(&options)?;
Ok(0)
@@ -658,6 +659,7 @@ enum CliCommand {
GlossaryUpdate,
GlossaryApprove,
GlossaryDeprecate,
GlossaryDelete,
GlossaryDiagnose,
Repack,
PublishLocalized,
@@ -1182,6 +1184,7 @@ const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
const RPC_METHOD_GLOSSARY_UPDATE: &str = "translation.glossary.update";
const RPC_METHOD_GLOSSARY_APPROVE: &str = "translation.glossary.approve";
const RPC_METHOD_GLOSSARY_DEPRECATE: &str = "translation.glossary.deprecate";
const RPC_METHOD_GLOSSARY_DELETE: &str = "translation.glossary.delete";
const RPC_METHOD_GLOSSARY_DIAGNOSE: &str = "translation.glossary.diagnose";
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
const RPC_METHOD_LOCALIZED_PUBLISH: &str = "localized.publish";
@@ -2296,6 +2299,15 @@ fn dispatch_rpc_method(
},
),
),
RPC_METHOD_GLOSSARY_DELETE => glossary_rpc_envelope(
request_id,
glossary_delete_rpc_report(
state_dir,
&tasks.base_config.output_root,
tasks.translation_worker_config.glossary_path.as_deref(),
request.params.as_ref(),
),
),
RPC_METHOD_TRANSLATION_WORKER_RUN => {
let config = match rpc_translation_worker_config_with_defaults(
request.params.as_ref(),
@@ -7474,6 +7486,7 @@ fn parse_args_with_env(
| CliCommand::GlossaryUpdate
| CliCommand::GlossaryApprove
| CliCommand::GlossaryDeprecate
| CliCommand::GlossaryDelete
| CliCommand::GlossaryDiagnose
| CliCommand::TranslationSet
| CliCommand::TranslationValidate
@@ -7629,6 +7642,7 @@ fn parse_args_with_env(
| CliCommand::GlossaryUpdate
| CliCommand::GlossaryApprove
| CliCommand::GlossaryDeprecate
| CliCommand::GlossaryDelete
| CliCommand::GlossaryDiagnose => {
if options.watch
|| options.daemon
@@ -7686,6 +7700,16 @@ fn parse_args_with_env(
));
}
}
CliCommand::GlossaryDelete => {
if options.glossary_term_id.is_none()
|| options.glossary_reviewer.is_none()
|| options.glossary_reason.is_none()
{
return Err(anyhow::anyhow!(
"Glossary delete 必须指定 --glossary-term-id、--glossary-reviewer 和 --glossary-reason"
));
}
}
_ => unreachable!(),
}
options.progress = false;
@@ -8293,6 +8317,7 @@ fn parse_translation_glossary_command(
"update" | "edit" => CliCommand::GlossaryUpdate,
"approve" | "trust" => CliCommand::GlossaryApprove,
"deprecate" | "retire" => CliCommand::GlossaryDeprecate,
"delete" | "remove" => CliCommand::GlossaryDelete,
"diagnose" | "check" => CliCommand::GlossaryDiagnose,
other => {
return Err(anyhow::anyhow!(
+87
View File
@@ -425,6 +425,20 @@ fn glossary_commands_parse_and_validate() {
])
.unwrap();
assert_eq!(diagnose.command, CliCommand::GlossaryDiagnose);
let delete = parse(&[
"bat",
"i18n",
"glossary",
"delete",
"--glossary-term-id",
"term-sensei",
"--glossary-reviewer",
"operator",
"--glossary-reason",
"duplicate",
])
.unwrap();
assert_eq!(delete.command, CliCommand::GlossaryDelete);
assert!(parse(&["bat", "i18n", "glossary", "diagnose"]).is_err());
assert!(parse(&[
"bat",
@@ -2784,6 +2798,79 @@ fn dispatch_glossary_summary_reports_missing_database_without_creating_it() {
assert!(!output_root.join("glossary.sqlite").exists());
}
#[test]
fn dispatch_glossary_delete_removes_term_and_returns_snapshot() {
let temp = tempfile::TempDir::new().unwrap();
let output_root = temp.path().join("output");
let state_dir = temp.path().join("state");
let glossary_path = output_root.join("glossary.sqlite");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
let repository = bat_infrastructure::SqliteGlossaryRepository::new(&glossary_path)
.await
.unwrap();
repository
.add(bat_core::domain::GlossaryTermDraft {
term_id: "term-sensei".to_string(),
definition: bat_core::domain::GlossaryTermSnapshot {
source_term: "Sensei".to_string(),
aliases: vec!["Teacher".to_string()],
recommended_translation: "老师".to_string(),
allowed_translations: Vec::new(),
source_language: None,
target_language: None,
category: Some("person".to_string()),
priority: 10,
scope: Default::default(),
},
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
source: bat_core::domain::GlossarySourceRecord {
source_kind: bat_core::domain::GlossarySourceKind::Manual,
source_ref: Some("test".to_string()),
source_author: Some("tester".to_string()),
source_note: None,
observed_unix_seconds: 100,
},
})
.await
.unwrap();
});
let context = test_task_context_with_config(OfficialUpdateConfig {
output_root: output_root.clone(),
..Default::default()
});
let envelope = dispatch_rpc_method(
&rpc_request(
"translation.glossary.delete",
Some(serde_json::json!({
"term_id": "term-sensei",
"reviewer": "reviewer",
"reason": "duplicate"
})),
),
&state_dir,
&new_daemon_control(),
&context,
"req-glossary-delete-1".to_string(),
);
let value = serde_json::to_value(envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["deleted"], true);
assert_eq!(value["data"]["term"]["term_id"], "term-sensei");
assert_eq!(value["data"]["term"]["source_term"], "Sensei");
runtime.block_on(async {
let repository = bat_infrastructure::SqliteGlossaryRepository::open(&glossary_path)
.await
.unwrap();
assert!(repository.find("term-sensei").await.is_err());
});
}
#[test]
fn dispatch_translation_memory_rejects_invalid_params_with_stable_error_code() {
let temp = tempfile::TempDir::new().unwrap();
@@ -65,6 +65,19 @@ pub(super) fn run_glossary_command(options: &CliOptions) -> anyhow::Result<()> {
options.glossary_reason.clone(),
)?
}
CliCommand::GlossaryDelete => {
let term_id = options.glossary_term_id.as_deref().ok_or_else(|| {
anyhow::anyhow!("Glossary delete 必须指定 --glossary-term-id")
})?;
let reviewer = options.glossary_reviewer.as_deref().ok_or_else(|| {
anyhow::anyhow!("Glossary delete 必须指定 --glossary-reviewer")
})?;
let reason = options
.glossary_reason
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Glossary delete 必须指定 --glossary-reason"))?;
build_glossary_delete_report(&path, term_id, reviewer, reason)?
}
_ => unreachable!(),
};
print_json_value(options.output_format, &report)
@@ -78,6 +91,7 @@ fn glossary_method(command: CliCommand) -> anyhow::Result<&'static str> {
CliCommand::GlossaryUpdate => RPC_METHOD_GLOSSARY_UPDATE,
CliCommand::GlossaryApprove => RPC_METHOD_GLOSSARY_APPROVE,
CliCommand::GlossaryDeprecate => RPC_METHOD_GLOSSARY_DEPRECATE,
CliCommand::GlossaryDelete => RPC_METHOD_GLOSSARY_DELETE,
CliCommand::GlossaryDiagnose => RPC_METHOD_GLOSSARY_DIAGNOSE,
_ => return Err(anyhow::anyhow!("不是 Glossary 命令")),
})
@@ -159,6 +173,20 @@ fn glossary_cli_params(options: &CliOptions) -> anyhow::Result<Option<serde_json
params.insert("reason".to_string(), serde_json::json!(reason));
}
}
CliCommand::GlossaryDelete => {
params.insert(
"term_id".to_string(),
serde_json::json!(options.glossary_term_id.as_deref().unwrap_or_default()),
);
params.insert(
"reviewer".to_string(),
serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()),
);
params.insert(
"reason".to_string(),
serde_json::json!(options.glossary_reason.as_deref().unwrap_or_default()),
);
}
_ => unreachable!(),
}
Ok(Some(serde_json::Value::Object(params)))
@@ -414,6 +442,33 @@ pub(super) fn build_glossary_review_report(
}))
}
pub(super) fn build_glossary_delete_report(
path: &std::path::Path,
term_id: &str,
reviewer: &str,
reason: &str,
) -> anyhow::Result<serde_json::Value> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let term = runtime.block_on(async {
let repository = SqliteGlossaryRepository::open(path)
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
repository
.delete(term_id, reviewer, reason)
.await
.map_err(|error| anyhow::anyhow!("{error}"))
})?;
Ok(serde_json::json!({
"available": true,
"path": path,
"schema_version": GLOSSARY_SCHEMA_VERSION,
"deleted": true,
"term": term,
}))
}
pub(super) fn glossary_rpc_envelope(
request_id: String,
result: Result<serde_json::Value, ApiError>,
@@ -546,6 +601,30 @@ pub(super) fn glossary_review_rpc_report(
.map_err(|error| glossary_internal_error(method, error))
}
pub(super) fn glossary_delete_rpc_report(
state_dir: &Path,
output_root: &Path,
default_path: Option<&Path>,
params: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ApiError> {
let params = glossary_params(params, RPC_METHOD_GLOSSARY_DELETE)?;
let term_id = glossary_string(&params, "term_id", RPC_METHOD_GLOSSARY_DELETE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 term_id"))?;
let reviewer = glossary_string(&params, "reviewer", RPC_METHOD_GLOSSARY_DELETE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reviewer"))?;
let reason = glossary_string(&params, "reason", RPC_METHOD_GLOSSARY_DELETE)?
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reason"))?;
let path = glossary_rpc_path(
state_dir,
output_root,
default_path,
Some(&serde_json::Value::Object(params.clone())),
RPC_METHOD_GLOSSARY_DELETE,
)?;
build_glossary_delete_report(&path, term_id, reviewer, reason)
.map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_DELETE, error))
}
fn glossary_rpc_path(
state_dir: &Path,
output_root: &Path,
@@ -348,6 +348,7 @@ Commands:
i18n glossary summary/query Show project Glossary terms and review counts
i18n glossary add/update Add or replace one Glossary term definition
i18n glossary approve/deprecate Review one Glossary term
i18n glossary delete Remove one Glossary term with reviewer and reason
i18n glossary diagnose Run deterministic Glossary QA for one TextUnit source
i18n publish Publish a localized release from a workbench or worker results
i18n rollback Roll back the current localized release
@@ -439,13 +440,13 @@ Sync:
--translation-provider <NAME> / --provider <NAME> Provider for i18n worker run (mock/crowdin)
--translation-fixture <PATH> Mock/provider fixture for i18n worker run
--glossary-path <PATH> Project Glossary SQLite path
--glossary-term-id <ID> Glossary term ID for add/update/review
--glossary-term-id <ID> Glossary term ID for add/update/review/delete
--glossary-source-term <TEXT> Source spelling for a Glossary term
--glossary-recommended-translation <TEXT> Recommended target translation
--glossary-source-text <TEXT> Source TextUnit text for Glossary query/diagnose
--glossary-context-json <JSON> TextUnit context for Glossary diagnose
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews
--glossary-reason <TEXT> Reason for Glossary review or override
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews/delete
--glossary-reason <TEXT> Reason for Glossary review/delete or override
--glossary-provenance <TEXT> Provenance for an explicit Glossary override
--worker-concurrency <N> Translation worker concurrency (default: 8, range 1..=256)
--worker-max-attempts <N> Maximum claims per translation task