diff --git a/internal/app/runner.go b/internal/app/runner.go index bb325285..0753f543 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -143,7 +143,6 @@ func newCommandRunnerWithFlags(loader cli.CatalogLoader, flags *GlobalFlags) exe scanner: newRuntimeContentScanner(), enforceContentScan: runtimeFlagEnabled(os.Getenv(runtimeContentScanEnforceEnv), false), includeScanReport: runtimeFlagEnabled(os.Getenv(runtimeContentScanReportOutputEnv), false), - auditSink: setupAuditSink(), } } @@ -456,6 +455,16 @@ func (r *runtimeRunner) executeInvocation(ctx context.Context, endpoint string, return r.executeStdioInvocation(ctx, invocation) } + // Constructing the Cobra tree is also used for help, schema, and command + // discovery. Open the process-wide audit writer only when a real invocation + // reaches the execution boundary so read-only command inspection does not + // leave an audit lock handle behind (which prevents TempDir cleanup on + // Windows). Keep an injected sink when tests or editions provide one. + auditSink := r.auditSink + if auditSink == nil { + auditSink = setupAuditSink() + } + invokeStart := time.Now() execID := generateExecutionID() r.transport.ExecutionId = execID @@ -483,7 +492,7 @@ func (r *runtimeRunner) executeInvocation(ctx context.Context, endpoint string, logging.LogCommandEnd(fl, execID, invocation.CanonicalProduct, invocation.Tool, retErr == nil, time.Since(invokeStart), errCat, errReason) - emitAudit(r.auditSink, execID, invokeStart, invocation, endpoint, retErr, version) + emitAudit(auditSink, execID, invokeStart, invocation, endpoint, retErr, version) }() // Check if this product has plugin-level auth credentials registered. diff --git a/internal/app/usage_recording_test.go b/internal/app/usage_recording_test.go index ad5ca8b7..5cc6cbfe 100644 --- a/internal/app/usage_recording_test.go +++ b/internal/app/usage_recording_test.go @@ -15,6 +15,8 @@ package app import ( "context" + "os" + "path/filepath" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/usage" @@ -100,7 +102,8 @@ func TestCrossPlatformCoverageRecordingToolCaller(t *testing.T) { } func TestCrossPlatformCoverageRootPublishesShortcutCommands(t *testing.T) { - t.Setenv("DWS_CONFIG_DIR", t.TempDir()) + configDir := t.TempDir() + t.Setenv("DWS_CONFIG_DIR", configDir) root := NewRootCommand(context.Background()) for _, path := range [][]string{{"shortcut", "list"}, {"calendar", "+today"}} { cmd, remaining, err := root.Find(path) @@ -108,4 +111,7 @@ func TestCrossPlatformCoverageRootPublishesShortcutCommands(t *testing.T) { t.Fatalf("root.Find(%v) = cmd=%v remaining=%v err=%v", path, cmd, remaining, err) } } + if _, err := os.Stat(filepath.Join(configDir, "audit", ".audit.lock")); !os.IsNotExist(err) { + t.Fatalf("constructing the root command opened an audit lock: %v", err) + } }