feat(i18n): 推进多语言剩余切片闭环

- 补齐 DataGrid、DataViewer、DefinitionViewer、JVM 等模块多语言文案与回归测试
- 收口 JVM 前后端展示、诊断、监控和资源呈现相关多语言路径
- 更新六语言共享词典并保留 raw 边界
This commit is contained in:
tianqijiuyun-latiao
2026-06-16 12:40:33 +08:00
parent 558966a129
commit 5fc29a6fd3
69 changed files with 6551 additions and 1276 deletions

View File

@@ -5,7 +5,7 @@ import (
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"errors"
"path/filepath"
"strings"
"time"
@@ -39,6 +39,25 @@ type jvmPreviewConfirmationContext struct {
AfterVersion string `json:"afterVersion"`
}
type jvmPreviewConfirmationHashError struct {
key string
err error
}
func (e *jvmPreviewConfirmationHashError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
func (e *jvmPreviewConfirmationHashError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
func buildJVMCapabilityError(mode string, cfg connection.ConnectionConfig, err error) jvm.Capability {
probeCfg := cfg
probeCfg.JVM.PreferredMode = mode
@@ -72,7 +91,7 @@ func resolveJVMProviderForMode(cfg connection.ConnectionConfig, mode string) (co
func (a *App) issueJVMPreviewConfirmationToken(cfg connection.ConnectionConfig, req jvm.ChangeRequest, preview jvm.ChangePreview) (string, error) {
contextHash, err := buildJVMPreviewConfirmationContextHash(cfg, req, preview)
if err != nil {
return "", err
return "", a.localizeJVMPreviewConfirmationHashError(err)
}
token := uuid.NewString()
@@ -101,17 +120,17 @@ func (a *App) consumeJVMPreviewConfirmationToken(cfg connection.ConnectionConfig
}
if strings.TrimSpace(preview.ConfirmationToken) == "" {
return fmt.Errorf("预览确认令牌缺失,请重新预览后再提交")
return errors.New(a.appText("jvm.backend.error.preview_confirmation_missing", nil))
}
token := strings.TrimSpace(req.ConfirmationToken)
if token == "" {
return fmt.Errorf("缺少确认令牌,请先完成预览确认")
return errors.New(a.appText("jvm.backend.error.confirmation_token_missing", nil))
}
expectedHash, err := buildJVMPreviewConfirmationContextHash(cfg, req, preview)
if err != nil {
return err
return a.localizeJVMPreviewConfirmationHashError(err)
}
now := time.Now()
@@ -119,21 +138,21 @@ func (a *App) consumeJVMPreviewConfirmationToken(cfg connection.ConnectionConfig
if a.jvmPreviewTokens == nil {
a.jvmPreviewTokens = make(map[string]jvmPreviewConfirmationToken)
}
a.pruneExpiredJVMPreviewConfirmationTokensLocked(now)
entry, ok := a.jvmPreviewTokens[token]
if ok {
delete(a.jvmPreviewTokens, token)
}
a.pruneExpiredJVMPreviewConfirmationTokensLocked(now)
a.jvmPreviewTokenMu.Unlock()
if !ok {
return fmt.Errorf("确认令牌已失效,请重新预览并确认")
return errors.New(a.appText("jvm.backend.error.confirmation_token_invalid", nil))
}
if !entry.expiresAt.After(now) {
return fmt.Errorf("确认令牌已过期,请重新预览并确认")
return errors.New(a.appText("jvm.backend.error.confirmation_token_expired", nil))
}
if subtle.ConstantTimeCompare([]byte(entry.contextHash), []byte(expectedHash)) != 1 {
return fmt.Errorf("确认令牌不匹配,请重新预览并确认")
return errors.New(a.appText("jvm.backend.error.confirmation_token_invalid", nil))
}
return nil
}
@@ -149,11 +168,17 @@ func (a *App) pruneExpiredJVMPreviewConfirmationTokensLocked(now time.Time) {
func buildJVMPreviewConfirmationContextHash(cfg connection.ConnectionConfig, req jvm.ChangeRequest, preview jvm.ChangePreview) (string, error) {
configHash, err := hashJSONValue(cfg)
if err != nil {
return "", fmt.Errorf("生成 JVM 预览上下文失败: %w", err)
return "", &jvmPreviewConfirmationHashError{
key: "jvm.backend.error.preview_context_hash_failed",
err: err,
}
}
payloadHash, err := hashJSONValue(req.Payload)
if err != nil {
return "", fmt.Errorf("生成 JVM 预览载荷摘要失败: %w", err)
return "", &jvmPreviewConfirmationHashError{
key: "jvm.backend.error.preview_payload_hash_failed",
err: err,
}
}
input := jvmPreviewConfirmationContext{
@@ -173,6 +198,51 @@ func buildJVMPreviewConfirmationContextHash(cfg connection.ConnectionConfig, req
return hashJSONValue(input)
}
func (a *App) localizeJVMPreviewConfirmationHashError(err error) error {
var hashErr *jvmPreviewConfirmationHashError
if !errors.As(err, &hashErr) || hashErr == nil {
return err
}
return errors.New(a.appText(hashErr.key, map[string]any{
"detail": hashErr.Error(),
}))
}
func (a *App) localizeJVMError(err error) string {
if err == nil {
return ""
}
var localized *jvm.LocalizedError
if errors.As(err, &localized) && localized != nil && strings.TrimSpace(localized.Key) != "" {
return a.appText(localized.Key, localized.Params)
}
return err.Error()
}
func (a *App) localizeJVMBlockingReason(preview jvm.ChangePreview) string {
reason := strings.TrimSpace(preview.BlockingReason)
if reason == "" {
return ""
}
key := strings.TrimSpace(preview.BlockingReasonLocalizationKey())
if key != "" && reason == key {
return a.appText(key, nil)
}
return reason
}
func (a *App) localizeJVMCapability(capability jvm.Capability) jvm.Capability {
reason := strings.TrimSpace(capability.Reason)
if reason == "" {
return capability
}
key := strings.TrimSpace(capability.ReasonLocalizationKey())
if key != "" && reason == key {
capability.Reason = a.appText(key, nil)
}
return capability
}
func hashJSONValue(value any) (string, error) {
encoded, err := json.Marshal(value)
if err != nil {
@@ -192,7 +262,7 @@ func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.Quer
return connection.QueryResult{Success: false, Message: jvm.DescribeConnectionTestError(normalized, err)}
}
return connection.QueryResult{Success: true, Message: "JVM 连接成功"}
return connection.QueryResult{Success: true, Message: a.appText("jvm.backend.message.connect_success", nil)}
}
func (a *App) JVMListResources(cfg connection.ConnectionConfig, parentPath string) connection.QueryResult {
@@ -232,12 +302,12 @@ func (a *App) JVMPreviewChange(cfg connection.ConnectionConfig, req jvm.ChangeRe
normalized, provider, err := resolveJVMProviderForMode(cfg, req.ProviderMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
preview, err := jvm.BuildChangePreview(a.ctx, provider, normalized, req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
if preview.Allowed && preview.RequiresConfirmation {
token, err := a.issueJVMPreviewConfirmationToken(normalized, req, preview)
@@ -246,6 +316,7 @@ func (a *App) JVMPreviewChange(cfg connection.ConnectionConfig, req jvm.ChangeRe
}
preview.ConfirmationToken = token
}
preview.BlockingReason = a.localizeJVMBlockingReason(preview)
return connection.QueryResult{Success: true, Data: preview}
}
@@ -259,17 +330,17 @@ func (a *App) JVMApplyChange(cfg connection.ConnectionConfig, req jvm.ChangeRequ
normalized, provider, err := resolveJVMProviderForMode(cfg, req.ProviderMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
preview, err := jvm.BuildChangePreview(a.ctx, provider, normalized, req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
if !preview.Allowed {
message := strings.TrimSpace(preview.BlockingReason)
message := a.localizeJVMBlockingReason(preview)
if message == "" {
message = "当前变更被 Guard 拦截"
message = a.appText("jvm.backend.error.change_blocked_by_guard", nil)
}
return connection.QueryResult{Success: false, Message: message}
}
@@ -302,7 +373,7 @@ func (a *App) JVMApplyChange(cfg connection.ConnectionConfig, req jvm.ChangeRequ
if message == "" {
return warning
}
return message + "" + warning
return message + a.appText("jvm.backend.separator.message_warning", nil) + warning
}
pendingTimestamp := time.Now().UnixMilli()
@@ -315,13 +386,13 @@ func (a *App) JVMApplyChange(cfg connection.ConnectionConfig, req jvm.ChangeRequ
}
if err := appendAudit("pending", pendingTimestamp); err != nil {
return connection.QueryResult{Success: false, Message: "审计记录写入失败,已阻止 JVM 变更: " + err.Error()}
return connection.QueryResult{Success: false, Message: a.appText("jvm.backend.error.audit_write_blocked", map[string]any{"detail": err.Error()})}
}
result, err := provider.ApplyChange(a.ctx, normalized, req)
if err != nil {
if auditErr := appendAudit("failed", terminalAuditTimestamp()); auditErr != nil {
return connection.QueryResult{Success: false, Message: err.Error() + ";失败审计写入失败: " + auditErr.Error()}
return connection.QueryResult{Success: false, Message: appendWarning(err.Error(), a.appText("jvm.backend.warning.failed_audit_write_failed", map[string]any{"detail": auditErr.Error()}))}
}
return connection.QueryResult{Success: false, Message: err.Error()}
}
@@ -331,7 +402,7 @@ func (a *App) JVMApplyChange(cfg connection.ConnectionConfig, req jvm.ChangeRequ
terminalResult = "applied"
}
if err := appendAudit(terminalResult, terminalAuditTimestamp()); err != nil {
result.Message = appendWarning(result.Message, "终态审计写入失败: "+err.Error())
result.Message = appendWarning(result.Message, a.appText("jvm.backend.warning.terminal_audit_write_failed", map[string]any{"detail": err.Error()}))
return connection.QueryResult{Success: true, Message: result.Message, Data: result}
}
@@ -369,7 +440,9 @@ func (a *App) JVMProbeCapabilities(cfg connection.ConnectionConfig) connection.Q
continue
}
items = append(items, caps...)
for _, cap := range caps {
items = append(items, a.localizeJVMCapability(cap))
}
}
return connection.QueryResult{Success: true, Data: items}

View File

@@ -39,7 +39,9 @@ func resolveJVMDiagnosticTransport(cfg connection.ConnectionConfig) (connection.
return connection.ConnectionConfig{}, nil, err
}
if !diagCfg.Enabled {
return connection.ConnectionConfig{}, nil, errors.New("当前连接未启用 JVM 诊断增强模式")
return connection.ConnectionConfig{}, nil, &jvm.LocalizedError{
Key: "jvm.backend.diagnostic.error.disabled",
}
}
normalized.JVM.Diagnostic = diagCfg
@@ -53,12 +55,12 @@ func resolveJVMDiagnosticTransport(cfg connection.ConnectionConfig) (connection.
func (a *App) JVMProbeDiagnosticCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
items, err := transport.ProbeCapabilities(a.ctx, normalized)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
return connection.QueryResult{Success: true, Data: items}
}
@@ -66,12 +68,12 @@ func (a *App) JVMProbeDiagnosticCapabilities(cfg connection.ConnectionConfig) co
func (a *App) JVMStartDiagnosticSession(cfg connection.ConnectionConfig, req jvm.DiagnosticSessionRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
handle, err := transport.StartSession(a.ctx, normalized, req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
return connection.QueryResult{Success: true, Data: handle}
}
@@ -79,7 +81,7 @@ func (a *App) JVMStartDiagnosticSession(cfg connection.ConnectionConfig, req jvm
func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, req jvm.DiagnosticCommandRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
redactor := jvm.NewDiagnosticOutputRedactor()
@@ -91,10 +93,10 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
req.Reason = strings.TrimSpace(req.Reason)
if req.SessionID == "" {
return connection.QueryResult{Success: false, Message: "诊断会话 ID 不能为空,请先创建会话"}
return connection.QueryResult{Success: false, Message: a.appText("jvm.backend.diagnostic.error.session_id_required", nil)}
}
if req.Command == "" {
return connection.QueryResult{Success: false, Message: "诊断命令不能为空"}
return connection.QueryResult{Success: false, Message: a.appText("jvm.backend.diagnostic.error.command_required", nil)}
}
if req.CommandID == "" {
req.CommandID = fmt.Sprintf("diag-%d", time.Now().UnixNano())
@@ -105,7 +107,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
commandType, err := jvm.ValidateDiagnosticExecutionPolicy(normalized, req.Command)
if err != nil {
message := redactor.RedactContent(req.SessionID, req.CommandID, err.Error())
message := redactor.RedactContent(req.SessionID, req.CommandID, a.localizeJVMError(err))
return connection.QueryResult{Success: false, Message: message}
}
riskLevel := diagnosticRiskLevel(commandType)
@@ -124,7 +126,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
RiskLevel: riskLevel,
Status: "running",
}); err != nil {
return connection.QueryResult{Success: false, Message: "诊断审计记录写入失败,已阻止命令执行: " + err.Error()}
return connection.QueryResult{Success: false, Message: a.appText("jvm.backend.diagnostic.error.audit_write_blocked", map[string]any{"detail": err.Error()})}
}
terminalSeen := false
@@ -145,7 +147,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
RiskLevel: riskLevel,
Status: status,
}); err != nil {
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
auditWarnings = append(auditWarnings, a.appText("jvm.backend.diagnostic.warning.audit_write_failed", map[string]any{"detail": err.Error()}))
}
}
@@ -156,6 +158,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
}
chunk.SessionID = req.SessionID
chunk.CommandID = req.CommandID
chunk = a.localizeDiagnosticChunkContent(chunk)
chunk = redactor.RedactChunk(chunk)
a.emitDiagnosticChunk(tabID, chunk)
if isDiagnosticTerminalPhase(chunk.Phase) {
@@ -166,10 +169,10 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
if err := transport.ExecuteCommand(a.ctx, normalized, req); err != nil {
phase := "failed"
if strings.Contains(strings.ToLower(err.Error()), "canceled") {
if isDiagnosticCanceledError(err) {
phase = "canceled"
}
redactedError := redactor.RedactContent(req.SessionID, req.CommandID, err.Error())
redactedError := redactor.RedactContent(req.SessionID, req.CommandID, a.localizeJVMError(err))
if !terminalSeen {
chunk := jvm.DiagnosticEventChunk{
SessionID: req.SessionID,
@@ -182,7 +185,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
a.emitDiagnosticChunk(tabID, chunk)
appendTerminalAudit(phase)
}
return connection.QueryResult{Success: false, Message: joinDiagnosticMessages(redactedError, auditWarnings)}
return connection.QueryResult{Success: false, Message: a.joinDiagnosticMessages(redactedError, auditWarnings)}
}
if !terminalSeen {
@@ -191,7 +194,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
CommandID: req.CommandID,
Event: "diagnostic",
Phase: "completed",
Content: "诊断命令执行完成",
Content: a.appText("jvm.backend.diagnostic.message.command_completed", nil),
Timestamp: time.Now().UnixMilli(),
}
a.emitDiagnosticChunk(tabID, chunk)
@@ -200,7 +203,7 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
return connection.QueryResult{
Success: true,
Message: joinDiagnosticMessages("", auditWarnings),
Message: a.joinDiagnosticMessages("", auditWarnings),
Data: map[string]any{
"sessionId": req.SessionID,
"commandId": req.CommandID,
@@ -212,17 +215,17 @@ func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID
func (a *App) JVMCancelDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, sessionID string, commandID string) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
sessionID = strings.TrimSpace(sessionID)
commandID = strings.TrimSpace(commandID)
if sessionID == "" || commandID == "" {
return connection.QueryResult{Success: false, Message: "取消命令缺少 sessionId 或 commandId"}
return connection.QueryResult{Success: false, Message: a.appText("jvm.backend.diagnostic.error.cancel_identifiers_required", nil)}
}
if err := transport.CancelCommand(a.ctx, normalized, sessionID, commandID); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
a.emitDiagnosticChunk(tabID, jvm.DiagnosticEventChunk{
@@ -230,7 +233,7 @@ func (a *App) JVMCancelDiagnosticCommand(cfg connection.ConnectionConfig, tabID
CommandID: commandID,
Event: "diagnostic",
Phase: "canceling",
Content: "已发送取消请求,等待诊断桥接端结束命令",
Content: a.appText("jvm.backend.diagnostic.message.cancel_requested", nil),
Timestamp: time.Now().UnixMilli(),
})
return connection.QueryResult{
@@ -261,6 +264,24 @@ func (a *App) emitDiagnosticChunk(tabID string, chunk jvm.DiagnosticEventChunk)
})
}
func (a *App) localizeDiagnosticChunkContent(chunk jvm.DiagnosticEventChunk) jvm.DiagnosticEventChunk {
if chunk.Metadata == nil {
return chunk
}
contentKey, ok := chunk.Metadata["contentKey"].(string)
contentKey = strings.TrimSpace(contentKey)
if !ok || contentKey == "" {
return chunk
}
var params map[string]any
if rawParams, ok := chunk.Metadata["contentParams"].(map[string]any); ok {
params = rawParams
}
chunk.Content = a.appText(contentKey, params)
return chunk
}
func diagnosticRiskLevel(commandType string) string {
switch strings.TrimSpace(commandType) {
case jvm.DiagnosticCommandCategoryObserve:
@@ -272,6 +293,20 @@ func diagnosticRiskLevel(commandType string) string {
}
}
func isDiagnosticCanceledError(err error) bool {
if err == nil {
return false
}
var localized *jvm.LocalizedError
if errors.As(err, &localized) && localized != nil {
switch strings.TrimSpace(localized.Key) {
case "jvm.backend.diagnostic.arthas.command_canceled":
return true
}
}
return strings.Contains(strings.ToLower(err.Error()), "canceled")
}
func isDiagnosticTerminalPhase(phase string) bool {
switch strings.ToLower(strings.TrimSpace(phase)) {
case "completed", "failed", "canceled":
@@ -281,7 +316,7 @@ func isDiagnosticTerminalPhase(phase string) bool {
}
}
func joinDiagnosticMessages(primary string, warnings []string) string {
func (a *App) joinDiagnosticMessages(primary string, warnings []string) string {
items := make([]string, 0, 1+len(warnings))
if strings.TrimSpace(primary) != "" {
items = append(items, strings.TrimSpace(primary))
@@ -292,5 +327,5 @@ func joinDiagnosticMessages(primary string, warnings []string) string {
}
items = append(items, strings.TrimSpace(warning))
}
return strings.Join(items, "")
return strings.Join(items, a.appText("jvm.backend.separator.message_warning", nil))
}

View File

@@ -20,6 +20,7 @@ type fakeDiagnosticTransport struct {
startErr error
executeReq jvm.DiagnosticCommandRequest
executeErr error
executeHook func()
executeCalls int
cancelSession string
cancelCommand string
@@ -41,6 +42,9 @@ func (f fakeDiagnosticTransport) StartSession(context.Context, connection.Connec
}
func (f fakeDiagnosticTransport) ExecuteCommand(context.Context, connection.ConnectionConfig, jvm.DiagnosticCommandRequest) error {
if f.executeHook != nil {
f.executeHook()
}
return f.executeErr
}
@@ -101,6 +105,25 @@ func (f *fakeStreamingDiagnosticTransport) CloseSession(context.Context, connect
return nil
}
func captureJVMDiagnosticChunks(t *testing.T, app *App) (*[]diagnosticChunkEventPayload, func()) {
t.Helper()
app.ctx = context.Background()
emitted := make([]diagnosticChunkEventPayload, 0, 2)
prevEmitter := emitJVMDiagnosticRuntimeEvent
emitJVMDiagnosticRuntimeEvent = func(ctx context.Context, eventName string, optionalData ...interface{}) {
if eventName != diagnosticChunkEvent {
return
}
payload, ok := optionalData[0].(diagnosticChunkEventPayload)
if !ok {
t.Fatalf("expected diagnostic chunk event payload, got %#v", optionalData[0])
}
emitted = append(emitted, payload)
}
return &emitted, func() { emitJVMDiagnosticRuntimeEvent = prevEmitter }
}
func TestJVMProbeDiagnosticCapabilitiesReturnsTransportPayload(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
@@ -178,6 +201,365 @@ func TestJVMStartDiagnosticSessionReturnsHandle(t *testing.T) {
}
}
func TestJVMDiagnosticStaticMessagesUseEnglishAppText(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return fakeDiagnosticTransport{}, nil
})
defer restore()
disabledRes := app.JVMProbeDiagnosticCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: false,
},
},
})
if disabledRes.Success || disabledRes.Message != "JVM diagnostic enhancement is not enabled for this connection" {
t.Fatalf("expected localized disabled message, got %+v", disabledRes)
}
cfg := connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}
sessionRes := app.JVMExecuteDiagnosticCommand(cfg, "tab-diag-1", jvm.DiagnosticCommandRequest{
Command: "thread -n 5",
})
if sessionRes.Success || sessionRes.Message != "Diagnostic session ID is required. Create a session first." {
t.Fatalf("expected localized session required message, got %+v", sessionRes)
}
commandRes := app.JVMExecuteDiagnosticCommand(cfg, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
})
if commandRes.Success || commandRes.Message != "Diagnostic command cannot be empty" {
t.Fatalf("expected localized command required message, got %+v", commandRes)
}
cancelRes := app.JVMCancelDiagnosticCommand(cfg, "tab-diag-1", "sess-1", " ")
if cancelRes.Success || cancelRes.Message != "Cancel command requires sessionId and commandId" {
t.Fatalf("expected localized cancel identifiers message, got %+v", cancelRes)
}
}
func TestJVMExecuteDiagnosticCommandLocalizesAuditWriteBlockedWithRawDetail(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
blockerPath := filepath.Join(tempDir, "audit-blocker")
if err := os.WriteFile(blockerPath, []byte("blocker"), 0o600); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
app.configDir = blockerPath
recorder := &fakeDiagnosticTransport{}
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return diagnosticTransportRecorder{recorder: recorder}, nil
})
defer restore()
res := app.JVMExecuteDiagnosticCommand(connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
CommandID: "cmd-observe-audit",
Command: "thread -n 5",
Source: "manual",
Reason: "observe threads",
})
expectedPrefix := "Failed to write diagnostic audit record, command execution was blocked: "
if res.Success || !strings.HasPrefix(res.Message, expectedPrefix) {
t.Fatalf("expected localized audit blocked message, got %+v", res)
}
if !strings.Contains(res.Message, blockerPath) {
t.Fatalf("expected raw audit error detail to include %q, got %q", blockerPath, res.Message)
}
if recorder.executeCalls != 0 {
t.Fatalf("expected transport ExecuteCommand not called, got %d", recorder.executeCalls)
}
}
func TestJVMExecuteDiagnosticCommandLocalizesAuditWarningWithEnglishSeparatorAndRawErrors(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
app.configDir = filepath.Join(tempDir, "audit-root")
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return fakeDiagnosticTransport{
executeErr: errors.New("bridge exploded"),
executeHook: func() {
if err := os.RemoveAll(app.configDir); err != nil {
t.Fatalf("RemoveAll returned error: %v", err)
}
if err := os.WriteFile(app.configDir, []byte("terminal-blocker"), 0o600); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
},
}, nil
})
defer restore()
res := app.JVMExecuteDiagnosticCommand(connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
CommandID: "cmd-observe-warning",
Command: "thread -n 5",
Source: "manual",
Reason: "observe threads",
})
if res.Success {
t.Fatalf("expected execute failure, got %+v", res)
}
if !strings.HasPrefix(res.Message, "bridge exploded; Failed to write audit record: ") {
t.Fatalf("expected raw execute error and localized warning joined by English separator, got %q", res.Message)
}
if !strings.Contains(res.Message, app.configDir) {
t.Fatalf("expected raw audit warning detail to include %q, got %q", app.configDir, res.Message)
}
}
func TestJVMExecuteDiagnosticCommandLocalizesTransportLocalizedError(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
emitted, restoreEmitter := captureJVMDiagnosticChunks(t, app)
defer restoreEmitter()
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return fakeDiagnosticTransport{
executeErr: &jvm.LocalizedError{
Key: "jvm.backend.diagnostic.arthas.base_url_invalid",
Params: map[string]any{"detail": "://raw-bad-url"},
},
}, nil
})
defer restore()
res := app.JVMExecuteDiagnosticCommand(connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
CommandID: "cmd-localized-error",
Command: "thread -n 5",
Source: "manual",
Reason: "observe threads",
})
if res.Success {
t.Fatalf("expected execute failure, got %+v", res)
}
if res.Message != "Arthas Tunnel address is invalid: ://raw-bad-url" {
t.Fatalf("expected localized transport error with raw detail, got %q", res.Message)
}
if strings.Contains(res.Message, "jvm.backend.diagnostic.arthas.base_url_invalid") {
t.Fatalf("expected user message not to expose i18n key, got %q", res.Message)
}
if len(*emitted) != 1 {
t.Fatalf("expected one failed chunk, got %#v", *emitted)
}
if (*emitted)[0].Chunk.Content != res.Message {
t.Fatalf("expected failed chunk content to match localized message, got %#v", (*emitted)[0].Chunk)
}
}
func TestJVMExecuteDiagnosticCommandLocalizesChunkContentKeyAndKeepsRunningOutputRaw(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
emitted, restoreEmitter := captureJVMDiagnosticChunks(t, app)
defer restoreEmitter()
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return &fakeStreamingDiagnosticTransport{
chunks: []jvm.DiagnosticEventChunk{
{
Event: "diagnostic",
Phase: "running",
Content: "Arthas raw output: 中文 and targetId=orders-prod-01",
},
{
Event: "diagnostic",
Phase: "completed",
Content: "",
Metadata: map[string]any{
"transport": jvm.DiagnosticTransportArthasTunnel,
"contentKey": "jvm.backend.diagnostic.message.arthas_command_completed",
"rawTargetID": "orders-prod-01",
},
},
},
}, nil
})
defer restore()
res := app.JVMExecuteDiagnosticCommand(connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
CommandID: "cmd-content-key",
Command: "thread -n 5",
Source: "manual",
Reason: "observe threads",
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
if len(*emitted) != 2 {
t.Fatalf("expected running and completed chunks, got %#v", *emitted)
}
if (*emitted)[0].Chunk.Content != "Arthas raw output: 中文 and targetId=orders-prod-01" {
t.Fatalf("expected running output to remain raw, got %#v", (*emitted)[0].Chunk)
}
completed := (*emitted)[1].Chunk
if completed.Content != "Arthas command completed" {
t.Fatalf("expected contentKey-localized completed content, got %#v", completed)
}
if completed.Metadata["contentKey"] != "jvm.backend.diagnostic.message.arthas_command_completed" {
t.Fatalf("expected contentKey metadata to be preserved, got %#v", completed.Metadata)
}
if completed.Metadata["rawTargetID"] != "orders-prod-01" {
t.Fatalf("expected raw metadata to be preserved, got %#v", completed.Metadata)
}
}
func TestJVMExecuteDiagnosticCommandEmitsEnglishCompletedChunk(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
emitted, restoreEmitter := captureJVMDiagnosticChunks(t, app)
defer restoreEmitter()
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return fakeDiagnosticTransport{}, nil
})
defer restore()
res := app.JVMExecuteDiagnosticCommand(connection.ConnectionConfig{
ID: "conn-orders",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
AllowObserveCommands: true,
},
},
}, "tab-diag-1", jvm.DiagnosticCommandRequest{
SessionID: "sess-1",
CommandID: "cmd-completed",
Command: "thread -n 5",
Source: "manual",
Reason: "observe threads",
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
if len(*emitted) != 1 {
t.Fatalf("expected one completed chunk, got %#v", *emitted)
}
if (*emitted)[0].Chunk.Content != "Diagnostic command completed" {
t.Fatalf("expected localized completed chunk content, got %#v", (*emitted)[0].Chunk)
}
}
func TestJVMCancelDiagnosticCommandEmitsEnglishCancelChunk(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
emitted, restoreEmitter := captureJVMDiagnosticChunks(t, app)
defer restoreEmitter()
restore := swapJVMDiagnosticTransportFactory(func(mode string) (jvm.DiagnosticTransport, error) {
return fakeDiagnosticTransport{}, nil
})
defer restore()
res := app.JVMCancelDiagnosticCommand(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
Diagnostic: connection.JVMDiagnosticConfig{
Enabled: true,
Transport: jvm.DiagnosticTransportAgentBridge,
BaseURL: "http://127.0.0.1:19091/gonavi/diag",
},
},
}, "tab-diag-1", "sess-1", "cmd-1")
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
if len(*emitted) != 1 {
t.Fatalf("expected one cancel chunk, got %#v", *emitted)
}
if (*emitted)[0].Chunk.Content != "Cancel request sent; waiting for the diagnostic bridge to stop the command" {
t.Fatalf("expected localized cancel chunk content, got %#v", (*emitted)[0].Chunk)
}
}
func TestJVMExecuteDiagnosticCommandReturnsAccepted(t *testing.T) {
app := NewAppWithSecretStore(nil)
recorder := &fakeDiagnosticTransport{}
@@ -248,7 +630,7 @@ func TestJVMExecuteDiagnosticCommandBlocksTraceWhenConnectionReadOnly(t *testing
if res.Success {
t.Fatalf("expected trace command to be blocked in read-only mode, got %+v", res)
}
if !strings.Contains(res.Message, "只读") {
if !strings.Contains(res.Message, "read-only") {
t.Fatalf("expected read-only message, got %+v", res)
}
if recorder.executeCalls != 0 {
@@ -290,7 +672,7 @@ func TestJVMExecuteDiagnosticCommandBlocksMutatingWhenConnectionReadOnly(t *test
if res.Success {
t.Fatalf("expected mutating command to be blocked in read-only mode, got %+v", res)
}
if !strings.Contains(res.Message, "只读") {
if !strings.Contains(res.Message, "read-only") {
t.Fatalf("expected read-only message, got %+v", res)
}
if recorder.executeCalls != 0 {
@@ -617,7 +999,7 @@ func TestJVMExecuteDiagnosticCommandFailsClosedWhenAuditWriteFails(t *testing.T)
if res.Success {
t.Fatalf("expected command to fail closed when initial audit write fails, got %+v", res)
}
if !strings.Contains(res.Message, "审计") {
if !strings.Contains(res.Message, "audit") {
t.Fatalf("expected audit failure message, got %+v", res)
}
if recorder.executeCalls != 0 {

View File

@@ -20,32 +20,32 @@ var currentJVMMonitoringManager jvmMonitoringService = jvm.NewMonitoringManager(
func (a *App) JVMStartMonitoring(cfg connection.ConnectionConfig) connection.QueryResult {
snapshot, err := currentJVMMonitoringManager.Start(a.ctx, cfg, "")
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
return connection.QueryResult{Success: true, Data: snapshot}
return connection.QueryResult{Success: true, Data: a.localizeJVMMonitoringSnapshot(snapshot)}
}
func (a *App) JVMGetMonitoringHistory(cfg connection.ConnectionConfig, providerMode string) connection.QueryResult {
connectionID, resolvedMode, err := resolveJVMMonitoringLookup(cfg, providerMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
snapshot, err := currentJVMMonitoringManager.GetHistory(connectionID, resolvedMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
return connection.QueryResult{Success: true, Data: snapshot}
return connection.QueryResult{Success: true, Data: a.localizeJVMMonitoringSnapshot(snapshot)}
}
func (a *App) JVMStopMonitoring(cfg connection.ConnectionConfig, providerMode string) connection.QueryResult {
connectionID, resolvedMode, err := resolveJVMMonitoringLookup(cfg, providerMode)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
if err := currentJVMMonitoringManager.Stop(connectionID, resolvedMode); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
return connection.QueryResult{Success: false, Message: a.localizeJVMError(err)}
}
return connection.QueryResult{Success: true, Data: map[string]any{
"connectionId": connectionID,
@@ -54,6 +54,23 @@ func (a *App) JVMStopMonitoring(cfg connection.ConnectionConfig, providerMode st
}}
}
func (a *App) localizeJVMMonitoringSnapshot(snapshot jvm.MonitoringSessionSnapshot) jvm.MonitoringSessionSnapshot {
if len(snapshot.ProviderWarnings) == 0 {
return snapshot
}
warnings := append([]string(nil), snapshot.ProviderWarnings...)
for index, warning := range warnings {
key, params, ok := jvm.ParseMonitoringProviderWarning(warning)
if !ok {
continue
}
warnings[index] = a.appText(key, params)
}
snapshot.ProviderWarnings = warnings
return snapshot
}
func resolveJVMMonitoringLookup(cfg connection.ConnectionConfig, requestedMode string) (string, string, error) {
normalized, resolvedMode, err := jvm.ResolveProviderMode(cfg, requestedMode)
if err != nil {

View File

@@ -3,6 +3,7 @@ package app
import (
"context"
"errors"
"strings"
"testing"
"GoNavi-Wails/internal/connection"
@@ -145,3 +146,165 @@ func TestJVMStopMonitoringReturnsManagerError(t *testing.T) {
t.Fatalf("unexpected manager stop args: connection=%q mode=%q", manager.stopConnection, manager.stopMode)
}
}
func TestJVMMonitoringMethodsLocalizeManagerLocalizedErrors(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
manager := &fakeJVMMonitoringManager{
startErr: &jvm.LocalizedError{
Key: "jvm.backend.monitoring.error.snapshot_unsupported",
Params: map[string]any{
"provider": "JMX",
},
},
historyErr: &jvm.LocalizedError{
Key: "jvm.backend.monitoring.error.session_not_found",
Params: map[string]any{
"connectionId": "conn-history",
"providerMode": jvm.ModeJMX,
},
},
stopErr: &jvm.LocalizedError{
Key: "jvm.backend.monitoring.error.session_not_found",
Params: map[string]any{
"connectionId": "conn-stop",
"providerMode": jvm.ModeAgent,
},
},
}
restore := swapJVMMonitoringManager(manager)
defer restore()
startRes := app.JVMStartMonitoring(connection.ConnectionConfig{
ID: "conn-monitor",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: jvm.ModeJMX,
AllowedModes: []string{jvm.ModeJMX},
},
})
assertMonitoringEnglishMessage(t, startRes, "JMX monitoring snapshot is not supported yet")
historyRes := app.JVMGetMonitoringHistory(connection.ConnectionConfig{
ID: "conn-history",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: jvm.ModeJMX,
AllowedModes: []string{jvm.ModeJMX},
},
}, "")
assertMonitoringEnglishMessage(t, historyRes, "Monitoring session not found for conn-history jmx")
stopRes := app.JVMStopMonitoring(connection.ConnectionConfig{
ID: "conn-stop",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: jvm.ModeAgent,
AllowedModes: []string{jvm.ModeAgent},
},
}, "")
assertMonitoringEnglishMessage(t, stopRes, "Monitoring session not found for conn-stop agent")
}
func TestJVMMonitoringMethodsLocalizeStructuredProviderWarnings(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
manager := &fakeJVMMonitoringManager{
startSnapshot: jvm.MonitoringSessionSnapshot{
ConnectionID: "conn-monitor",
ProviderMode: jvm.ModeJMX,
Running: false,
ProviderWarnings: []string{
"endpoint cpu metric unavailable",
"__gonavi_i18n__:jvm.backend.monitoring.warning.sample_auto_stopped:count=3",
},
},
historySnapshot: jvm.MonitoringSessionSnapshot{
ConnectionID: "conn-monitor",
ProviderMode: jvm.ModeJMX,
Running: false,
ProviderWarnings: []string{
"__gonavi_i18n__:jvm.backend.monitoring.warning.sample_auto_stopped:count=3",
"collector returned HTTP 503",
},
},
}
restore := swapJVMMonitoringManager(manager)
defer restore()
startRes := app.JVMStartMonitoring(connection.ConnectionConfig{
ID: "conn-monitor",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: jvm.ModeJMX,
AllowedModes: []string{jvm.ModeJMX},
},
})
startSnapshot := assertMonitoringSnapshot(t, startRes)
assertMonitoringWarnings(t, startSnapshot.ProviderWarnings, []string{
"endpoint cpu metric unavailable",
"Monitoring sampling failed 3 consecutive times and this session was stopped automatically",
})
historyRes := app.JVMGetMonitoringHistory(connection.ConnectionConfig{
ID: "conn-monitor",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: jvm.ModeJMX,
AllowedModes: []string{jvm.ModeJMX},
},
}, "")
historySnapshot := assertMonitoringSnapshot(t, historyRes)
assertMonitoringWarnings(t, historySnapshot.ProviderWarnings, []string{
"Monitoring sampling failed 3 consecutive times and this session was stopped automatically",
"collector returned HTTP 503",
})
}
func assertMonitoringEnglishMessage(t *testing.T, res connection.QueryResult, want string) {
t.Helper()
if res.Success {
t.Fatalf("expected monitoring method to fail, got %+v", res)
}
if res.Message != want {
t.Fatalf("expected monitoring message %q, got %+v", want, res)
}
if strings.Contains(res.Message, "jvm.backend.") {
t.Fatalf("expected localized message instead of raw key, got %q", res.Message)
}
}
func assertMonitoringSnapshot(t *testing.T, res connection.QueryResult) jvm.MonitoringSessionSnapshot {
t.Helper()
if !res.Success {
t.Fatalf("expected monitoring method to succeed, got %+v", res)
}
snapshot, ok := res.Data.(jvm.MonitoringSessionSnapshot)
if !ok {
t.Fatalf("expected monitoring snapshot, got %#v", res.Data)
}
return snapshot
}
func assertMonitoringWarnings(t *testing.T, got []string, want []string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("expected warnings %#v, got %#v", want, got)
}
for index, expected := range want {
if got[index] != expected {
t.Fatalf("expected warning %d to be %q, got %#v", index, expected, got)
}
if strings.Contains(got[index], "jvm.backend.") {
t.Fatalf("expected localized warning instead of raw key, got %#v", got)
}
}
}

View File

@@ -85,8 +85,40 @@ func forceAuditAppendFailureAfterPending(t *testing.T, auditDir string) {
}
}
func expectedAuditAppendError(t *testing.T, auditRoot string) string {
t.Helper()
err := jvm.NewAuditStore(filepath.Join(auditRoot, "jvm_audit.jsonl")).Append(jvm.AuditRecord{
Timestamp: 1,
ConnectionID: "conn-orders",
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "expected raw audit append error",
Result: "pending",
})
if err == nil {
t.Fatalf("expected audit append to fail for %q", auditRoot)
}
return err.Error()
}
func assertEnglishJVMConfirmationMessage(t *testing.T, got string, want string) {
t.Helper()
if got != want {
t.Fatalf("expected English confirmation message %q, got %q", want, got)
}
for _, text := range []string{"确认", "令牌", "预览", "重新"} {
if strings.Contains(got, text) {
t.Fatalf("expected no Chinese confirmation text %q in message %q", text, got)
}
}
}
func TestTestJVMConnectionUsesPreferredProvider(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
var gotMode string
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
gotMode = mode
@@ -109,8 +141,8 @@ func TestTestJVMConnectionUsesPreferredProvider(t *testing.T) {
if gotMode != "endpoint" {
t.Fatalf("expected provider mode endpoint, got %q", gotMode)
}
if res.Message != "JVM 连接成功" {
t.Fatalf("expected success message %q, got %q", "JVM 连接成功", res.Message)
if res.Message != "JVM connection succeeded" {
t.Fatalf("expected success message %q, got %q", "JVM connection succeeded", res.Message)
}
}
@@ -277,6 +309,95 @@ func TestJVMProbeCapabilitiesIncludesReasonWhenProbeFails(t *testing.T) {
}
}
func TestJVMProbeCapabilitiesLocalizesBuiltInReadOnlyReasons(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
restore := swapJVMProviderFactory(jvm.NewProvider)
defer restore()
readOnly := true
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
Timeout: 3,
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx", "endpoint", "agent"},
JMX: connection.JVMJMXConfig{
Host: "127.0.0.1",
Port: 9010,
},
Endpoint: connection.JVMEndpointConfig{
BaseURL: "https://orders.internal/manage/jvm",
TimeoutSeconds: 3,
},
Agent: connection.JVMAgentConfig{
BaseURL: "https://orders.internal/agent",
TimeoutSeconds: 3,
},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 3 {
t.Fatalf("expected three capabilities, got %#v", res.Data)
}
for _, item := range items {
if item.CanWrite {
t.Fatalf("expected read-only capability for %s, got %#v", item.Mode, item)
}
if item.Reason != "Current connection is read-only, so writes are blocked" {
t.Fatalf("expected localized read-only reason for %s, got %#v", item.Mode, item)
}
if strings.Contains(item.Reason, "jvm.backend.") || strings.Contains(item.Reason, "只读") {
t.Fatalf("expected no key or Chinese text in reason for %s, got %q", item.Mode, item.Reason)
}
}
}
func TestJVMProbeCapabilitiesKeepsProviderReasonThatLooksLikeCatalogKeyRaw(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
providerReason := "jvm.backend.error.change_blocked_read_only"
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
probe: []jvm.Capability{{
Mode: jvm.ModeJMX,
CanBrowse: true,
CanWrite: false,
CanPreview: true,
DisplayLabel: "JMX",
Reason: providerReason,
}},
}, nil
})
defer restore()
res := app.JVMProbeCapabilities(connection.ConnectionConfig{
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
})
if !res.Success {
t.Fatalf("expected success, got %+v", res)
}
items, ok := res.Data.([]jvm.Capability)
if !ok || len(items) != 1 {
t.Fatalf("expected one capability, got %#v", res.Data)
}
if items[0].Reason != providerReason {
t.Fatalf("expected provider reason to stay raw, got %#v", items[0])
}
}
func TestJVMProbeCapabilitiesTranslatesJMXProbeErrorUsingCurrentMode(t *testing.T) {
app := NewAppWithSecretStore(nil)
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
@@ -480,6 +601,7 @@ func TestJVMGetValueReturnsProviderPayload(t *testing.T) {
func TestJVMApplyChangeRequiresConfirmationTokenForHighRiskPreview(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := false
var applyReq jvm.ChangeRequest
@@ -529,9 +651,7 @@ func TestJVMApplyChangeRequiresConfirmationTokenForHighRiskPreview(t *testing.T)
if res.Success {
t.Fatalf("expected missing confirmation token to fail, got %+v", res)
}
if !strings.Contains(res.Message, "确认") && !strings.Contains(res.Message, "重新预览") {
t.Fatalf("expected confirmation guidance message, got %q", res.Message)
}
assertEnglishJVMConfirmationMessage(t, res.Message, "Confirmation token is missing. Complete preview confirmation first.")
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
}
@@ -601,6 +721,212 @@ func TestJVMApplyChangeReturnsProviderPayload(t *testing.T) {
}
}
func TestJVMApplyChangeUsesEnglishGuardFallbackWhenBlockingReasonEmpty(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := false
var applyReq jvm.ChangeRequest
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
value: jvm.ValueSnapshot{
ResourceID: "/cache/orders",
Kind: "entry",
Format: "json",
},
previewSet: true,
preview: jvm.ChangePreview{
Allowed: false,
},
applyReq: &applyReq,
apply: jvm.ApplyResult{Status: "applied"},
}, nil
})
defer restore()
res := app.JVMApplyChange(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{"status": "ready"},
})
if res.Success {
t.Fatalf("expected guard-blocked apply to fail, got %+v", res)
}
if res.Message != "The current change was blocked by Guard" {
t.Fatalf("expected English guard fallback message, got %q", res.Message)
}
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
}
}
func TestJVMProviderBlockingReasonThatLooksLikeCatalogKeyStaysRaw(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := false
providerReason := "jvm.backend.error.change_blocked_read_only"
var applyReq jvm.ChangeRequest
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
previewSet: true,
preview: jvm.ChangePreview{
Allowed: false,
BlockingReason: providerReason,
},
applyReq: &applyReq,
apply: jvm.ApplyResult{Status: "applied"},
}, nil
})
defer restore()
cfg := connection.ConnectionConfig{
Type: "jvm",
ID: "conn-provider-reason",
Host: "orders.internal",
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}
req := jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{"status": "ready"},
}
previewRes := app.JVMPreviewChange(cfg, req)
if !previewRes.Success {
t.Fatalf("expected blocked preview result, got %+v", previewRes)
}
preview, ok := previewRes.Data.(jvm.ChangePreview)
if !ok {
t.Fatalf("expected preview data, got %#v", previewRes.Data)
}
if preview.BlockingReason != providerReason {
t.Fatalf("expected provider blocking reason to stay raw, got %q", preview.BlockingReason)
}
applyRes := app.JVMApplyChange(cfg, req)
if applyRes.Success {
t.Fatalf("expected provider-blocked apply to fail, got %+v", applyRes)
}
if applyRes.Message != providerReason {
t.Fatalf("expected provider blocking message to stay raw, got %q", applyRes.Message)
}
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
}
}
func TestJVMPreviewChange(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
readOnly := true
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{}, nil
})
defer restore()
res := app.JVMPreviewChange(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-readonly",
Host: "orders.internal",
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{"status": "ready"},
})
if !res.Success {
t.Fatalf("expected read-only preview to return blocked preview, got %+v", res)
}
preview, ok := res.Data.(jvm.ChangePreview)
if !ok {
t.Fatalf("expected preview data, got %#v", res.Data)
}
if preview.Allowed {
t.Fatalf("expected preview to be blocked, got %#v", preview)
}
if preview.BlockingReason != "Current connection is read-only, so writes are blocked" {
t.Fatalf("expected localized read-only blocking reason, got %#v", preview)
}
if strings.Contains(preview.BlockingReason, "jvm.backend.") || strings.Contains(preview.BlockingReason, "只读") {
t.Fatalf("expected app boundary to localize blocking reason, got %q", preview.BlockingReason)
}
}
func TestJVMApplyChange(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := true
var applyReq jvm.ChangeRequest
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
applyReq: &applyReq,
apply: jvm.ApplyResult{Status: "applied"},
}, nil
})
defer restore()
res := app.JVMApplyChange(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-readonly",
Host: "orders.internal",
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{"status": "ready"},
})
if res.Success {
t.Fatalf("expected read-only apply to fail, got %+v", res)
}
if res.Message != "Current connection is read-only, so writes are blocked" {
t.Fatalf("expected localized read-only blocking reason, got %q", res.Message)
}
if strings.Contains(res.Message, "jvm.backend.") || strings.Contains(res.Message, "只读") {
t.Fatalf("expected app boundary to localize blocking reason, got %q", res.Message)
}
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
}
}
func TestJVMApplyChangePreviewTokenAllowsConfirmedApply(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.configDir = t.TempDir()
@@ -700,8 +1026,153 @@ func TestJVMApplyChangePreviewTokenAllowsConfirmedApply(t *testing.T) {
}
}
func TestIssueJVMPreviewConfirmationTokenLocalizesPayloadHashError(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
readOnly := false
_, err := app.issueJVMPreviewConfirmationToken(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
JVM: connection.JVMConfig{
Environment: jvm.EnvPROD,
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{
"callback": func() {},
},
}, jvm.ChangePreview{
Allowed: true,
RequiresConfirmation: true,
ConfirmationToken: "preview-token",
Summary: "risky change",
RiskLevel: "high",
})
if err == nil {
t.Fatalf("expected payload hash error")
}
got := err.Error()
want := "Failed to generate JVM preview payload digest: json: unsupported type: func()"
if got != want {
t.Fatalf("expected localized payload hash error %q, got %q", want, got)
}
if strings.Contains(got, "生成 JVM 预览载荷摘要失败") {
t.Fatalf("expected no Chinese payload hash prefix in message %q", got)
}
}
func TestJVMPreviewChangeLocalizesConfirmationTokenFailure(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
readOnly := false
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
previewSet: true,
preview: jvm.ChangePreview{
Allowed: true,
RequiresConfirmation: true,
Summary: "risky change",
RiskLevel: "high",
},
}, nil
})
defer restore()
res := app.JVMPreviewChange(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
JVM: connection.JVMConfig{
Environment: jvm.EnvPROD,
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{
"callback": func() {},
},
})
if res.Success {
t.Fatalf("expected preview failure, got %+v", res)
}
want := "Failed to generate JVM change confirmation token: json: unsupported type: func()"
if res.Message != want {
t.Fatalf("expected localized confirmation token error %q, got %q", want, res.Message)
}
if strings.Contains(res.Message, "生成 JVM 变更确认令牌失败") {
t.Fatalf("expected no Chinese confirmation token prefix in message %q", res.Message)
}
}
func TestJVMApplyChangeLocalizesConfirmationTokenFailure(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
readOnly := false
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
previewSet: true,
preview: jvm.ChangePreview{
Allowed: true,
RequiresConfirmation: true,
Summary: "risky change",
RiskLevel: "high",
},
}, nil
})
defer restore()
res := app.JVMApplyChange(connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
JVM: connection.JVMConfig{
Environment: jvm.EnvPROD,
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}, jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{
"callback": func() {},
},
})
if res.Success {
t.Fatalf("expected apply preview failure, got %+v", res)
}
want := "Failed to generate JVM change confirmation token: json: unsupported type: func()"
if res.Message != want {
t.Fatalf("expected localized confirmation token error %q, got %q", want, res.Message)
}
if strings.Contains(res.Message, "生成 JVM 变更确认令牌失败") {
t.Fatalf("expected no Chinese confirmation token prefix in message %q", res.Message)
}
}
func TestJVMApplyChangeRejectsUnissuedDeterministicConfirmationToken(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := false
var applyReq jvm.ChangeRequest
@@ -766,11 +1237,86 @@ func TestJVMApplyChangeRejectsUnissuedDeterministicConfirmationToken(t *testing.
if res.Success {
t.Fatalf("expected unissued confirmation token to fail, got %+v", res)
}
assertEnglishJVMConfirmationMessage(t, res.Message, "Confirmation token is invalid. Preview and confirm again.")
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
}
}
func TestJVMApplyChangeRejectsMismatchedPreviewConfirmationContext(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
readOnly := false
applyCalls := 0
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
return fakeJVMProvider{
value: jvm.ValueSnapshot{
ResourceID: "/cache/orders",
Kind: "entry",
Format: "json",
Value: map[string]any{
"status": "stale",
},
},
previewSet: true,
preview: jvm.ChangePreview{
Allowed: true,
RequiresConfirmation: true,
Summary: "risky change",
RiskLevel: "high",
},
applyFn: func(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ApplyResult, error) {
applyCalls++
return jvm.ApplyResult{Status: "applied"}, nil
},
}, nil
})
defer restore()
cfg := connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
JVM: connection.JVMConfig{
Environment: jvm.EnvPROD,
ReadOnly: &readOnly,
PreferredMode: "jmx",
AllowedModes: []string{"jmx"},
},
}
req := jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
Payload: map[string]any{
"status": "ready",
},
}
previewRes := app.JVMPreviewChange(cfg, req)
if !previewRes.Success {
t.Fatalf("expected preview success, got %+v", previewRes)
}
preview, ok := previewRes.Data.(jvm.ChangePreview)
if !ok {
t.Fatalf("expected preview payload, got %#v", previewRes.Data)
}
req.ConfirmationToken = preview.ConfirmationToken
req.ResourceID = "/cache/customers"
res := app.JVMApplyChange(cfg, req)
if res.Success {
t.Fatalf("expected mismatched confirmation context to fail, got %+v", res)
}
assertEnglishJVMConfirmationMessage(t, res.Message, "Confirmation token is invalid. Preview and confirm again.")
if applyCalls != 0 {
t.Fatalf("expected provider ApplyChange not to run, got %d calls", applyCalls)
}
}
func TestJVMApplyChangeRejectsReplayedPreviewConfirmationToken(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.configDir = t.TempDir()
@@ -848,6 +1394,7 @@ func TestJVMApplyChangeRejectsReplayedPreviewConfirmationToken(t *testing.T) {
func TestJVMApplyChangeRejectsExpiredPreviewConfirmationToken(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
app.configDir = t.TempDir()
app.jvmPreviewTokenTTL = time.Nanosecond
readOnly := false
@@ -914,6 +1461,7 @@ func TestJVMApplyChangeRejectsExpiredPreviewConfirmationToken(t *testing.T) {
if res.Success {
t.Fatalf("expected expired confirmation token to fail, got %+v", res)
}
assertEnglishJVMConfirmationMessage(t, res.Message, "Confirmation token expired. Preview and confirm again.")
if applyCalls != 0 {
t.Fatalf("expected provider ApplyChange not to run, got %d calls", applyCalls)
}
@@ -1074,10 +1622,11 @@ func TestJVMApplyChangeNormalizesRequestBeforeProviderAndAudit(t *testing.T) {
}
}
func TestJVMPreviewChangeRejectsModeOutsideAllowedModes(t *testing.T) {
func TestJVMPreviewChangeRejectsDisallowedProviderMode(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
res := app.JVMPreviewChange(connection.ConnectionConfig{
cfg := connection.ConnectionConfig{
Type: "jvm",
ID: "conn-orders",
Host: "orders.internal",
@@ -1085,18 +1634,33 @@ func TestJVMPreviewChangeRejectsModeOutsideAllowedModes(t *testing.T) {
PreferredMode: "endpoint",
AllowedModes: []string{"endpoint"},
},
}, jvm.ChangeRequest{
}
req := jvm.ChangeRequest{
ProviderMode: "jmx",
ResourceID: "/cache/orders",
Action: "put",
Reason: "repair cache",
})
}
res := app.JVMPreviewChange(cfg, req)
if res.Success {
t.Fatalf("expected preview request to be rejected, got %+v", res)
}
if !strings.Contains(res.Message, "不允许使用") {
t.Fatalf("expected disallowed mode error, got %+v", res)
want := "Current connection does not allow jmx mode"
if res.Message != want {
t.Fatalf("expected localized disallowed mode error %q, got %+v", want, res)
}
if strings.Contains(res.Message, "不允许使用") || strings.Contains(res.Message, "jvm.backend.") {
t.Fatalf("expected no Chinese or raw key in disallowed mode error, got %q", res.Message)
}
applyRes := app.JVMApplyChange(cfg, req)
if applyRes.Success {
t.Fatalf("expected apply request to be rejected, got %+v", applyRes)
}
if applyRes.Message != want {
t.Fatalf("expected localized apply disallowed mode error %q, got %+v", want, applyRes)
}
}
@@ -1132,12 +1696,14 @@ func TestJVMListAuditRecordsReturnsLatestRecords(t *testing.T) {
func TestJVMApplyChangeFailsClosedWhenInitialAuditWriteFails(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
blockerPath := filepath.Join(tempDir, "audit-blocker")
if err := os.WriteFile(blockerPath, []byte("blocker"), 0o600); err != nil {
t.Fatalf("WriteFile returned error: %v", err)
}
app.configDir = blockerPath
expectedDetail := expectedAuditAppendError(t, blockerPath)
readOnly := false
var applyReq jvm.ChangeRequest
@@ -1181,8 +1747,9 @@ func TestJVMApplyChangeFailsClosedWhenInitialAuditWriteFails(t *testing.T) {
if res.Success {
t.Fatalf("expected fail-closed when initial audit write fails, got %+v", res)
}
if !strings.Contains(res.Message, "审计") {
t.Fatalf("expected audit failure message, got %q", res.Message)
want := "Failed to write audit record, JVM change was blocked: " + expectedDetail
if res.Message != want {
t.Fatalf("expected English audit failure message %q, got %q", want, res.Message)
}
if applyReq.ResourceID != "" {
t.Fatalf("expected provider ApplyChange not to run, got %#v", applyReq)
@@ -1246,6 +1813,7 @@ func TestJVMApplyChangeLatestAuditRecordIsTerminal(t *testing.T) {
func TestJVMApplyChangeApplySuccessKeepsSuccessWhenTerminalAuditFails(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1302,13 +1870,16 @@ func TestJVMApplyChangeApplySuccessKeepsSuccessWhenTerminalAuditFails(t *testing
if result.Status != "applied" {
t.Fatalf("expected applied status, got %#v", result)
}
if !strings.Contains(result.Message, "终态审计写入失败") {
t.Fatalf("expected terminal audit warning in result message, got %#v", result)
expectedDetail := expectedAuditAppendError(t, auditDir)
want := "ok; Failed to write terminal audit record: " + expectedDetail
if result.Message != want {
t.Fatalf("expected terminal audit warning %q, got %#v", want, result)
}
}
func TestJVMApplyChangeApplyFailureReportsFailedAuditWriteError(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1358,11 +1929,10 @@ func TestJVMApplyChangeApplyFailureReportsFailedAuditWriteError(t *testing.T) {
if res.Success {
t.Fatalf("expected failure when apply fails, got %+v", res)
}
if !strings.Contains(res.Message, "provider apply failed") {
t.Fatalf("expected provider failure in message, got %q", res.Message)
}
if !strings.Contains(res.Message, "失败审计写入失败") {
t.Fatalf("expected failed audit write failure in message, got %q", res.Message)
expectedDetail := expectedAuditAppendError(t, auditDir)
want := "provider apply failed; Failed to write failure audit record: " + expectedDetail
if res.Message != want {
t.Fatalf("expected provider failure with failed audit warning %q, got %q", want, res.Message)
}
}
@@ -1412,6 +1982,7 @@ func TestJVMApplyChangeApplyFailureKeepsProviderErrorWhenFailedAuditSucceeds(t *
func TestJVMApplyChangeUsesProviderErrorWhenFailedAuditAlsoFails(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1457,16 +2028,16 @@ func TestJVMApplyChangeUsesProviderErrorWhenFailedAuditAlsoFails(t *testing.T) {
if res.Success {
t.Fatalf("expected failure when provider apply fails, got %+v", res)
}
if !strings.HasPrefix(res.Message, "provider apply failed") {
t.Fatalf("expected provider error prefix, got %q", res.Message)
}
if !strings.Contains(res.Message, "失败审计写入失败") {
t.Fatalf("expected failed audit write failure in message, got %q", res.Message)
expectedDetail := expectedAuditAppendError(t, auditDir)
want := "provider apply failed; Failed to write failure audit record: " + expectedDetail
if res.Message != want {
t.Fatalf("expected provider error with English failed audit warning %q, got %q", want, res.Message)
}
}
func TestJVMApplyChangeTerminalAuditWarningAppendsToExistingResultMessage(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1509,13 +2080,16 @@ func TestJVMApplyChangeTerminalAuditWarningAppendsToExistingResultMessage(t *tes
if !ok {
t.Fatalf("expected apply result data, got %#v", res.Data)
}
if !strings.Contains(result.Message, "provider message") || !strings.Contains(result.Message, "终态审计写入失败") {
t.Fatalf("expected provider message with terminal audit warning, got %#v", result)
expectedDetail := expectedAuditAppendError(t, auditDir)
want := "provider message; Failed to write terminal audit record: " + expectedDetail
if result.Message != want {
t.Fatalf("expected provider message with English terminal audit warning %q, got %#v", want, result)
}
}
func TestJVMApplyChangeTerminalAuditWarningUsesStandaloneMessageWhenResultMessageEmpty(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1558,13 +2132,16 @@ func TestJVMApplyChangeTerminalAuditWarningUsesStandaloneMessageWhenResultMessag
if !ok {
t.Fatalf("expected apply result data, got %#v", res.Data)
}
if !strings.Contains(result.Message, "终态审计写入失败") {
t.Fatalf("expected standalone terminal audit warning, got %#v", result)
expectedDetail := expectedAuditAppendError(t, auditDir)
want := "Failed to write terminal audit record: " + expectedDetail
if result.Message != want {
t.Fatalf("expected standalone English terminal audit warning %q, got %#v", want, result)
}
}
func TestJVMApplyChangeFailedAuditFailureMessageIncludesUnderlyingError(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1599,17 +2176,15 @@ func TestJVMApplyChangeFailedAuditFailureMessageIncludesUnderlyingError(t *testi
if res.Success {
t.Fatalf("expected failure when apply fails, got %+v", res)
}
if !strings.Contains(res.Message, "失败审计写入失败") {
t.Fatalf("expected failed audit failure marker, got %q", res.Message)
}
lowerMessage := strings.ToLower(res.Message)
if !strings.Contains(lowerMessage, "not a directory") && !strings.Contains(lowerMessage, "system cannot find the path specified") {
t.Fatalf("expected underlying audit failure detail in message, got %q", res.Message)
expectedDetail := expectedAuditAppendError(t, auditDir)
if !strings.Contains(res.Message, "Failed to write failure audit record: "+expectedDetail) {
t.Fatalf("expected underlying audit failure detail %q in message, got %q", expectedDetail, res.Message)
}
}
func TestJVMApplyChangeFailureMessageSeparatorUsesChineseSemicolon(t *testing.T) {
func TestJVMApplyChangeFailureMessageSeparatorUsesLocalizedEnglishSeparator(t *testing.T) {
app := NewAppWithSecretStore(nil)
app.SetLanguage("en-US")
tempDir := t.TempDir()
auditDir := filepath.Join(tempDir, "audit")
if err := os.MkdirAll(auditDir, 0o755); err != nil {
@@ -1644,8 +2219,11 @@ func TestJVMApplyChangeFailureMessageSeparatorUsesChineseSemicolon(t *testing.T)
if res.Success {
t.Fatalf("expected failure when apply fails, got %+v", res)
}
if !strings.Contains(res.Message, ";失败审计写入失败") {
t.Fatalf("expected chinese semicolon separator in failure message, got %q", res.Message)
if !strings.Contains(res.Message, "; Failed to write failure audit record: ") {
t.Fatalf("expected English separator in failure message, got %q", res.Message)
}
if strings.Contains(res.Message, "") {
t.Fatalf("expected no Chinese semicolon separator in failure message, got %q", res.Message)
}
}