From 91bd7c78027ba67ba85c03fb3e603daf63376068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=8D=93=E6=BE=8E?= Date: Fri, 17 Jul 2026 13:14:02 +0800 Subject: [PATCH] fix: emit structured audit verification output --- internal/app/audit_command.go | 27 ++++++++- internal/app/audit_output_contract_test.go | 65 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 internal/app/audit_output_contract_test.go diff --git a/internal/app/audit_command.go b/internal/app/audit_command.go index 4f75ab26..10ff49c2 100644 --- a/internal/app/audit_command.go +++ b/internal/app/audit_command.go @@ -13,6 +13,7 @@ import ( "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/spf13/cobra" ) @@ -117,9 +118,29 @@ func newAuditVerifyCommand() *cobra.Command { } } - valid, brokenAt, err := auditVerify(target) - if err != nil { - return fmt.Errorf("校验失败: %w", err) + valid, brokenAt, verifyErr := auditVerify(target) + if output.ResolveFormat(cmd, output.FormatTable) == output.FormatJSON { + if verifyErr != nil && brokenAt == 0 { + return fmt.Errorf("校验失败: %w", verifyErr) + } + payload := map[string]any{ + "valid": valid, + "file": target, + "brokenAt": brokenAt, + } + if verifyErr != nil { + payload["reason"] = verifyErr.Error() + } + if err := output.WriteCommandPayload(cmd, payload, output.FormatTable); err != nil { + return err + } + if !valid { + auditExit(1) + } + return nil + } + if verifyErr != nil { + return fmt.Errorf("校验失败: %w", verifyErr) } if valid { fmt.Printf("✓ %s 哈希链完整(全部通过)\n", filepath.Base(target)) diff --git a/internal/app/audit_output_contract_test.go b/internal/app/audit_output_contract_test.go new file mode 100644 index 00000000..47dc2bca --- /dev/null +++ b/internal/app/audit_output_contract_test.go @@ -0,0 +1,65 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package app + +import ( + "bytes" + "encoding/json" + "errors" + "testing" + + "github.com/spf13/cobra" +) + +func executeAuditVerifyJSON(t *testing.T, verify func(string) (bool, int, error)) (map[string]any, int, error) { + t.Helper() + previousVerify, previousExit := auditVerify, auditExit + t.Cleanup(func() { + auditVerify, auditExit = previousVerify, previousExit + }) + auditVerify = verify + exitCode := 0 + auditExit = func(code int) { exitCode = code } + + root := &cobra.Command{Use: "dws"} + root.SilenceErrors = true + root.SilenceUsage = true + root.PersistentFlags().String("format", "json", "output format") + root.AddCommand(newAuditVerifyCommand()) + var stdout bytes.Buffer + root.SetOut(&stdout) + root.SetArgs([]string{"verify", "--file", "/tmp/audit.jsonl"}) + err := root.Execute() + + var payload map[string]any + if decodeErr := json.Unmarshal(stdout.Bytes(), &payload); decodeErr != nil { + t.Fatalf("audit verify stdout must be one JSON document: %v\n%s", decodeErr, stdout.String()) + } + return payload, exitCode, err +} + +func TestCrossPlatformCoverageAuditVerifyJSONOutputIsSingleDocument(t *testing.T) { + payload, exitCode, err := executeAuditVerifyJSON(t, func(string) (bool, int, error) { + return true, 0, nil + }) + if err != nil || exitCode != 0 { + t.Fatalf("audit verify returned err=%v exit=%d", err, exitCode) + } + if payload["valid"] != true || payload["file"] != "/tmp/audit.jsonl" || payload["brokenAt"] != float64(0) { + t.Fatalf("unexpected audit payload: %#v", payload) + } +} + +func TestCrossPlatformCoverageAuditVerifyBrokenJSONIncludesReasonBeforeExit(t *testing.T) { + payload, exitCode, err := executeAuditVerifyJSON(t, func(string) (bool, int, error) { + return false, 3, errors.New("prev_hash mismatch") + }) + if err != nil || exitCode != 1 { + t.Fatalf("broken audit verify returned err=%v exit=%d", err, exitCode) + } + if payload["valid"] != false || payload["brokenAt"] != float64(3) || payload["reason"] != "prev_hash mismatch" { + t.Fatalf("unexpected broken audit payload: %#v", payload) + } +}