diff --git a/internal/app/plugin_commands_test.go b/internal/app/plugin_commands_test.go index 5584f152..097061a4 100644 --- a/internal/app/plugin_commands_test.go +++ b/internal/app/plugin_commands_test.go @@ -30,6 +30,7 @@ import ( apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes" "github.com/spf13/cobra" @@ -417,10 +418,8 @@ func TestConflictingPluginDescriptorCannotReplaceDistributionEndpoint(t *testing func TestSchemaSourceRootDoesNotLoadRuntimePlugins(t *testing.T) { isolatePluginRuntime(t) - previous := rootLoadPlugins - t.Cleanup(func() { rootLoadPlugins = previous }) var calls atomic.Int32 - rootLoadPlugins = func(*cobra.Command, *pipeline.Engine, executor.Runner) []*cobra.Command { + testseam.Swap(t, &rootLoadPlugins, func(*cobra.Command, *pipeline.Engine, executor.Runner) []*cobra.Command { calls.Add(1) AppendDynamicServer(conferencePluginDescriptor()) return buildPluginCommands( @@ -428,7 +427,7 @@ func TestSchemaSourceRootDoesNotLoadRuntimePlugins(t *testing.T) { executor.EchoRunner{}, nil, ) - } + }) base := NewSchemaSourceRootCommand() if calls.Load() != 0 { diff --git a/internal/app/plugin_registration_residual_test.go b/internal/app/plugin_registration_residual_test.go index ffbf83ad..c7457ff3 100644 --- a/internal/app/plugin_registration_residual_test.go +++ b/internal/app/plugin_registration_residual_test.go @@ -18,6 +18,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/plugin" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/userdef" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes" @@ -27,14 +28,11 @@ import ( type schemaSourceContextKey struct{} func TestSchemaSourceRootPropagatesContextWithoutLoadingPlugins(t *testing.T) { - previous := rootLoadPlugins - t.Cleanup(func() { rootLoadPlugins = previous }) - pluginLoads := 0 - rootLoadPlugins = func(*cobra.Command, *pipeline.Engine, executor.Runner) []*cobra.Command { + testseam.Swap(t, &rootLoadPlugins, func(*cobra.Command, *pipeline.Engine, executor.Runner) []*cobra.Command { pluginLoads++ return nil - } + }) wantContext := context.WithValue(context.Background(), schemaSourceContextKey{}, "schema") root := NewSchemaSourceRootCommand(wantContext) if root.Context() != wantContext { diff --git a/internal/app/root_help_test.go b/internal/app/root_help_test.go index 762e6f57..29b9680a 100644 --- a/internal/app/root_help_test.go +++ b/internal/app/root_help_test.go @@ -23,6 +23,7 @@ import ( "testing" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" ) @@ -161,9 +162,7 @@ func TestRootChatMediaUploadWithoutAppCredentialsReturnsMigrationValidation(t *t "--file", filePath, "--type", "image", } - previousArgs := os.Args - os.Args = append([]string{"dws"}, commandArgs...) - t.Cleanup(func() { os.Args = previousArgs }) + testseam.Swap(t, &os.Args, append([]string{"dws"}, commandArgs...)) root := NewRootCommand() var output bytes.Buffer diff --git a/internal/auth/rejected_token_refresh_test.go b/internal/auth/rejected_token_refresh_test.go index 42e7bda6..8b8ba731 100644 --- a/internal/auth/rejected_token_refresh_test.go +++ b/internal/auth/rejected_token_refresh_test.go @@ -22,6 +22,7 @@ import ( "testing" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -78,9 +79,7 @@ func installRejectedTokenHookStore(t *testing.T, data TokenData) *rejectedTokenH func installOAuthRefreshStub(t *testing.T, fn func(*OAuthProvider, context.Context, *TokenData) (*TokenData, error)) { t.Helper() resetRejectedTokenRefreshCoordinator(t) - previous := oauthRefreshToken - oauthRefreshToken = fn - t.Cleanup(func() { oauthRefreshToken = previous }) + testseam.Swap(t, &oauthRefreshToken, fn) } func resetRejectedTokenRefreshCoordinator(t *testing.T) { @@ -121,11 +120,10 @@ func installProfilesAcquireProbe(t *testing.T) <-chan struct{} { t.Helper() previous := profilesAcquireDualLock attempted := make(chan struct{}, 1) - profilesAcquireDualLock = func(ctx context.Context, configDir string) (*DualLock, error) { + testseam.Swap(t, &profilesAcquireDualLock, func(ctx context.Context, configDir string) (*DualLock, error) { attempted <- struct{}{} return previous(ctx, configDir) - } - t.Cleanup(func() { profilesAcquireDualLock = previous }) + }) return attempted } diff --git a/internal/cli/param_aliases_test.go b/internal/cli/param_aliases_test.go index 989fc055..aab959b7 100644 --- a/internal/cli/param_aliases_test.go +++ b/internal/cli/param_aliases_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -37,9 +38,7 @@ func conceptFixture() []Concept { func useParamConceptLoader(t *testing.T, concepts ParamConcepts, err error) { t.Helper() - previous := loadReviewedParamConcepts - loadReviewedParamConcepts = func() (ParamConcepts, error) { return concepts, err } - t.Cleanup(func() { loadReviewedParamConcepts = previous }) + testseam.Swap(t, &loadReviewedParamConcepts, func() (ParamConcepts, error) { return concepts, err }) } func TestReduceParamAliasesLoadsAndValidatesSourceTree(t *testing.T) { diff --git a/internal/cli/schema_cross_platform_coverage_final_test.go b/internal/cli/schema_cross_platform_coverage_final_test.go index d0e46054..cacae025 100644 --- a/internal/cli/schema_cross_platform_coverage_final_test.go +++ b/internal/cli/schema_cross_platform_coverage_final_test.go @@ -176,9 +176,7 @@ func TestCrossPlatformCoverageFinalChangedStatementGaps(t *testing.T) { }) t.Run("schemaRegistryFromSnapshot wire and typed failures", func(t *testing.T) { - prev := validateSchemaSnapshotTypedRoundTrip - validateSchemaSnapshotTypedRoundTrip = false - t.Cleanup(func() { validateSchemaSnapshotTypedRoundTrip = prev }) + testseam.Swap(t, &validateSchemaSnapshotTypedRoundTrip, false) snapshot := SchemaCatalogSnapshot{ Catalog: map[string]any{"kind": "schema", "level": "catalog", "source": "t", "count": 1, "tool_count": 1, "products": []any{ map[string]any{"id": "sample", "tools": []any{map[string]any{"canonical_path": "sample.run"}}}, diff --git a/internal/cli/schema_cross_platform_coverage_remaining_test.go b/internal/cli/schema_cross_platform_coverage_remaining_test.go index afde0d09..6716d12b 100644 --- a/internal/cli/schema_cross_platform_coverage_remaining_test.go +++ b/internal/cli/schema_cross_platform_coverage_remaining_test.go @@ -11,6 +11,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -1068,9 +1069,7 @@ func TestCrossPlatformCoverageSchemaCatalogAndSnapshotMoreBranches(t *testing.T) }) t.Run("schemaRegistryFromSnapshot minimal decode", func(t *testing.T) { - prev := validateSchemaSnapshotTypedRoundTrip - validateSchemaSnapshotTypedRoundTrip = false - t.Cleanup(func() { validateSchemaSnapshotTypedRoundTrip = prev }) + testseam.Swap(t, &validateSchemaSnapshotTypedRoundTrip, false) snapshot := SchemaCatalogSnapshot{ Catalog: map[string]any{"kind": "schema", "level": "catalog", "source": "t", "count": 1, "tool_count": 1, "products": []any{}}, Tools: map[string]map[string]any{}, diff --git a/internal/cli/schema_overall_gap_coverage_test.go b/internal/cli/schema_overall_gap_coverage_test.go index c8c3ae40..54eb90ad 100644 --- a/internal/cli/schema_overall_gap_coverage_test.go +++ b/internal/cli/schema_overall_gap_coverage_test.go @@ -1080,3 +1080,85 @@ func TestOverallCoverageGapRuntimeParamsAndAgentMetadata(t *testing.T) { t.Fatal("invalid dry-run registry must fail delivery validation") } } + +func TestCrossPlatformCoverageOverallRegressionRecovery(t *testing.T) { + if _, ok := lookupPinnedMCPParam(nil, "property", "flag"); ok { + t.Fatal("nil pinned params must miss") + } + if _, ok := lookupPinnedMCPParam(map[string]embeddedMCPParamMeta{}, "property", "flag"); ok { + t.Fatal("empty pinned params must miss") + } + if _, ok := lookupPinnedMCPParam(map[string]embeddedMCPParamMeta{"other": {Type: "string"}}, "property", "flag"); ok { + t.Fatal("unmatched pinned params must miss") + } + if _, ok := pinnedMCPMetadataForEntryFrom(runtimeSchemaEntry{}, agentMetadata{}, embeddedMCPMetadata{}); ok { + t.Fatal("empty MCP metadata must not match") + } + if _, ok := pinnedMCPMetadataForEntryFrom(runtimeSchemaEntry{}, agentMetadata{}, embeddedMCPMetadata{ + Tools: map[string]embeddedMCPToolMetadata{"other.key": {}}, + }); ok { + t.Fatal("missing MCP metadata keys must not match") + } + + left := runtimeSchemaStringCandidateAtPriority("same", true, "z-source", 5, "p") + right := runtimeSchemaStringCandidateAtPriority("same", true, "a-source", 5, "p") + winner, err := resolveRuntimeSchemaCandidate("source-order", left, right) + if err != nil || winner.Source != "a-source" { + t.Fatalf("source tie-break = %#v err=%v", winner, err) + } + + if validCommandRegistryCLIPath("bad!token") { + t.Fatal("invalid CLI path token must fail validation") + } + registry := EffectiveCommandRegistry{Commands: []CommandSpec{{ + CanonicalPath: "sample.run", PrimaryCLIPath: "sample run", + }}} + if got := registry.SourceHash(); got == "" { + t.Fatal("default visibility hash must be non-empty") + } + if got := stableUniqueStrings([]string{"", "a", "a", " b ", "b"}); len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("stableUniqueStrings = %#v", got) + } + + root := &cobra.Command{Use: "dws"} + root.AddCommand(&cobra.Command{Use: "help"}) + run := &cobra.Command{Use: "run", Run: func(*cobra.Command, []string) {}} + root.AddCommand(run) + visited := 0 + walkLeafCommands(root, func(*cobra.Command) { visited++ }) + if visited != 1 { + t.Fatalf("walkLeafCommands visited %d leaves, want 1", visited) + } + + provenance := commandRegistryIdentityProvenance(BoundCommandSpec{ + CommandSpec: CommandSpec{ + CanonicalPath: "sample.run", + PrimaryCLIPath: "sample run", + Source: "reviewed", + }, + PrimaryCommand: run, + AliasCommands: []BoundAlias{{Path: "sample alias", Command: nil}}, + }) + if provenance.Resolution != "registry_identity" { + t.Fatalf("identity provenance = %#v", provenance) + } + + if _, err := (SchemaRegistry{Products: []ProductSpec{{ID: "", Tools: []ToolSpec{}}}}).ToSnapshotPayload(); err == nil { + t.Fatal("empty product id must fail ToSnapshotPayload") + } + + prevExclusions := reviewedSchemaParameterMappingExclusions + t.Cleanup(func() { reviewedSchemaParameterMappingExclusions = prevExclusions }) + reviewedSchemaParameterMappingExclusions = map[string]string{} + if _, err := loadSchemaParameterBindingSnapshot(); err == nil { + t.Fatal("empty mapping exclusions ledger must fail load") + } + + cmd := NewSchemaCommand(nil) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + cmd.SetArgs([]string{"__coverage_gate_unknown_schema_path__"}) + if err := cmd.Execute(); err == nil { + t.Fatal("unknown schema path must fail schema command") + } +} diff --git a/internal/cli/schema_source_root_coverage_test.go b/internal/cli/schema_source_root_coverage_test.go index 98cf77a6..40fd88c4 100644 --- a/internal/cli/schema_source_root_coverage_test.go +++ b/internal/cli/schema_source_root_coverage_test.go @@ -11,6 +11,7 @@ import ( "testing/fstest" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -179,9 +180,7 @@ func TestCrossPlatformCoverageSchemaSourceRootErrorBranchesAndRestoreFallback(t t.Fatalf("materialize assemble error = %v", err) } - prevHook := restorePackageCLISchemaDeliveryHook - restorePackageCLISchemaDeliveryHook = nil - t.Cleanup(func() { restorePackageCLISchemaDeliveryHook = prevHook }) + testseam.Swap(t, &restorePackageCLISchemaDeliveryHook, nil) RestorePackageCLISchemaDeliveryForTest() if SchemaSourceRootRegistered() { t.Fatal("restore fallback must clear Schema source root") diff --git a/internal/event/lock/lock_test.go b/internal/event/lock/lock_test.go index ba61db4c..eab8109a 100644 --- a/internal/event/lock/lock_test.go +++ b/internal/event/lock/lock_test.go @@ -19,6 +19,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" ) func TestCrossPlatformCoverageTryAcquireErrorBranchesAndEmptyPath(t *testing.T) { @@ -30,9 +32,7 @@ func TestCrossPlatformCoverageTryAcquireErrorBranchesAndEmptyPath(t *testing.T) t.Fatal("empty lock path should be empty") } - previous := acquireFileLock - t.Cleanup(func() { acquireFileLock = previous }) - acquireFileLock = func(*os.File) error { return errors.New("unexpected lock failure") } + testseam.Swap(t, &acquireFileLock, func(*os.File) error { return errors.New("unexpected lock failure") }) if lock, err := TryAcquire(filepath.Join(t.TempDir(), "lock")); err == nil || lock != nil || errors.Is(err, ErrBusy) { t.Fatalf("TryAcquire injected failure = %#v, %v", lock, err) } diff --git a/internal/event/process/process_alive_unix_test.go b/internal/event/process/process_alive_unix_test.go index 0b33924c..044d334b 100644 --- a/internal/event/process/process_alive_unix_test.go +++ b/internal/event/process/process_alive_unix_test.go @@ -6,11 +6,12 @@ import ( "errors" "syscall" "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" ) func TestCrossPlatformCoverageAliveUnixErrorClassification(t *testing.T) { - previous := killProcess - t.Cleanup(func() { killProcess = previous }) + testseam.Protect(t, &killProcess) for _, tc := range []struct { err error want bool diff --git a/internal/helpers/attendance_coverage_more_test.go b/internal/helpers/attendance_coverage_more_test.go index fef3b340..5b73ecb8 100644 --- a/internal/helpers/attendance_coverage_more_test.go +++ b/internal/helpers/attendance_coverage_more_test.go @@ -5,15 +5,15 @@ import ( "io" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) func TestCrossPlatformCoverageAttendanceResponseAndDateCoverage(t *testing.T) { - previous := deps + testseam.Protect(t, &deps) InitDeps(&productExampleCaller{}) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) printGroupModifyDeeplink(nil) for _, value := range []map[string]any{ diff --git a/internal/helpers/chat_coverage_more_test.go b/internal/helpers/chat_coverage_more_test.go index a16ac2cb..7430479b 100644 --- a/internal/helpers/chat_coverage_more_test.go +++ b/internal/helpers/chat_coverage_more_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -337,12 +338,11 @@ func TestCrossPlatformCoverageChatFileUtilityCoverage(t *testing.T) { _ = unmarshalJSONUseNumber(`{"n":1}`, &map[string]any{}) _ = firstStringField(map[string]any{"one": "", "two": 2}, "one", "two") - previous := deps + testseam.Protect(t, &deps) caller := &helpersCoreCaller{format: "json"} InitDeps(caller) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) caller.format = "raw" } diff --git a/internal/helpers/cmdcore_phase2_coverage_test.go b/internal/helpers/cmdcore_phase2_coverage_test.go index 3d432b09..337219bf 100644 --- a/internal/helpers/cmdcore_phase2_coverage_test.go +++ b/internal/helpers/cmdcore_phase2_coverage_test.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -211,9 +212,7 @@ func TestCrossPlatformCoverageDeclareLeafMetadataValidateWithoutConfirmRunsInner // no-Validate/no-Caller fallback: without deps the deferred CallTool gate has // nothing to hook, so ConfirmSafety runs before the inner RunE (fail closed). func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t *testing.T) { - prev := deps - deps = nil - t.Cleanup(func() { deps = prev }) + testseam.Swap(t, &deps, nil) ran := false cmd := &cobra.Command{ @@ -265,10 +264,8 @@ func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t // is rejected. Post-RunE ConfirmSafety is intentionally not used — local side // effects may already have run. func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool(t *testing.T) { - prev := deps caller := &deferConfirmTestCaller{} - deps = &Deps{Caller: caller, Out: NewFormatter()} - t.Cleanup(func() { deps = prev }) + testseam.Swap(t, &deps, &Deps{Caller: caller, Out: NewFormatter()}) ran := 0 cmd := &cobra.Command{ diff --git a/internal/helpers/connect_attachment_prepare_test.go b/internal/helpers/connect_attachment_prepare_test.go index 23bcaa1e..855c8903 100644 --- a/internal/helpers/connect_attachment_prepare_test.go +++ b/internal/helpers/connect_attachment_prepare_test.go @@ -19,11 +19,12 @@ import ( "path/filepath" "strings" "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" ) func TestPrepareOpenCodeAttachmentsUsesStoryboardAndTranscript(t *testing.T) { - previous := generateConnectVideoStoryboard - t.Cleanup(func() { generateConnectVideoStoryboard = previous }) + testseam.Protect(t, &generateConnectVideoStoryboard) storyboard := filepath.Join(t.TempDir(), "storyboard.jpg") if err := os.WriteFile(storyboard, []byte("jpeg"), 0o600); err != nil { t.Fatal(err) @@ -61,11 +62,9 @@ func TestPrepareOpenCodeAttachmentsUsesStoryboardAndTranscript(t *testing.T) { } func TestPrepareOpenCodeAttachmentsDoesNotSubmitVideoWhenStoryboardFails(t *testing.T) { - previous := generateConnectVideoStoryboard - t.Cleanup(func() { generateConnectVideoStoryboard = previous }) - generateConnectVideoStoryboard = func(context.Context, string) (string, error) { + testseam.Swap(t, &generateConnectVideoStoryboard, func(context.Context, string) (string, error) { return "", os.ErrNotExist - } + }) prompt, attachments := prepareOpenCodeAttachments(context.Background(), "请看视频", []connectMediaAttachment{ {LocalPath: "/tmp/original.mov", FileName: "demo.mov", MediaType: "video"}, }) diff --git a/internal/helpers/coverage_pure_more_test.go b/internal/helpers/coverage_pure_more_test.go index 5384f241..c1fe9539 100644 --- a/internal/helpers/coverage_pure_more_test.go +++ b/internal/helpers/coverage_pure_more_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -235,12 +236,11 @@ func TestCrossPlatformCoverageChartMailAndSheetPureHelpersCoverage(t *testing.T) } func TestCrossPlatformCoverageSheetResponseCleanupCoverage(t *testing.T) { - previous := deps + testseam.Protect(t, &deps) caller := &helpersCoreCaller{format: "json"} InitDeps(caller) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) for _, text := range []string{ "", diff --git a/internal/helpers/doc_coverage_more_test.go b/internal/helpers/doc_coverage_more_test.go index 078eff98..2db96813 100644 --- a/internal/helpers/doc_coverage_more_test.go +++ b/internal/helpers/doc_coverage_more_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -81,11 +82,10 @@ func TestCrossPlatformCoverageDocUploadDownloadParsingCoverage(t *testing.T) { } func TestCrossPlatformCoverageResolveDocContentCoverage(t *testing.T) { - previous := deps + testseam.Protect(t, &deps) InitDeps(&productExampleCaller{}) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) newCommand := func() *cobra.Command { cmd := &cobra.Command{Use: "doc"} cmd.Flags().String("content-file", "", "") @@ -152,11 +152,10 @@ func TestCrossPlatformCoverageRunDocReadJSONMLCoverage(t *testing.T) { } func TestCrossPlatformCoverageDocDeprecationWrappersCoverage(t *testing.T) { - previous := deps + testseam.Protect(t, &deps) InitDeps(&productExampleCaller{}) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) for _, wrap := range []func(*cobra.Command){ func(cmd *cobra.Command) { wrapDocDeprecated(cmd, "drive target") }, func(cmd *cobra.Command) { wrapDocDeprecatedToWiki(cmd, "wiki target") }, diff --git a/internal/helpers/http_coverage_more_test.go b/internal/helpers/http_coverage_more_test.go index 0ccef7b0..d7bf6fd4 100644 --- a/internal/helpers/http_coverage_more_test.go +++ b/internal/helpers/http_coverage_more_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -133,8 +134,7 @@ func TestCrossPlatformCoverageDocVersionsCoverage(t *testing.T) { func jsonNumber(value string) json.Number { return json.Number(value) } func TestCrossPlatformCoverageRunDocReadJsonMLCoverage(t *testing.T) { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) caller := &helpersCoreCaller{format: "json"} InitDeps(caller) deps.Out.w = io.Discard diff --git a/internal/helpers/leaf_constraints_test.go b/internal/helpers/leaf_constraints_test.go index 1909d6e5..0102aa51 100644 --- a/internal/helpers/leaf_constraints_test.go +++ b/internal/helpers/leaf_constraints_test.go @@ -25,6 +25,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" ) func leafConstraintTestSpec(captured *map[string]any) LeafSpec { @@ -336,8 +337,7 @@ func TestCrossPlatformCoverageLeafSpecCorePaths(t *testing.T) { caller := &guardedMutationCaller{} serverRouted := func() error { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) InitDeps(caller) deps.Out.w = io.Discard cmd := NewLeafCommand(LeafSpec{ @@ -417,8 +417,7 @@ func TestCrossPlatformCoverageLeafSpecCorePaths(t *testing.T) { // 默认 product 路由(无 Server)+ LeafInt Required(走 leafHasEffectiveValue // 的 LeafInt 分支)+ Transform 返回 (nil,nil) 跳过键。 defaultRouted := func() error { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) oldArgs := os.Args os.Args = []string{"dws", "chat"} t.Cleanup(func() { os.Args = oldArgs }) diff --git a/internal/helpers/leaf_test.go b/internal/helpers/leaf_test.go index d883cd5b..cc773184 100644 --- a/internal/helpers/leaf_test.go +++ b/internal/helpers/leaf_test.go @@ -24,6 +24,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -464,8 +465,7 @@ func TestDeclareLeafMetadataInstallsConfirmSafetyForUserRequired(t *testing.T) { func TestDeclareLeafMetadataDefersConfirmUntilCallTool(t *testing.T) { // Without Validate: RunE-local checks run before ConfirmSafety; the gate // fires on the first MCP CallTool. - prev := deps - t.Cleanup(func() { deps = prev }) + testseam.Protect(t, &deps) concrete := &deferConfirmTestCaller{} InitDeps(concrete) deps.Out.w = io.Discard diff --git a/internal/helpers/markdown_ci_coverage_test.go b/internal/helpers/markdown_ci_coverage_test.go index 142fad18..ba721e18 100644 --- a/internal/helpers/markdown_ci_coverage_test.go +++ b/internal/helpers/markdown_ci_coverage_test.go @@ -15,6 +15,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -514,9 +515,7 @@ func TestMarkdownCICoverageGlobalDryRunAndNames(t *testing.T) { }) t.Run("nil command", func(t *testing.T) { - previous := deps - deps = nil - t.Cleanup(func() { deps = previous }) + testseam.Swap(t, &deps, nil) if markdownGlobalDryRun(nil) { t.Fatal("nil command unexpectedly enabled dry run") } diff --git a/internal/helpers/polling_coverage_more_test.go b/internal/helpers/polling_coverage_more_test.go index aadbb3d7..131ce939 100644 --- a/internal/helpers/polling_coverage_more_test.go +++ b/internal/helpers/polling_coverage_more_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -49,11 +50,10 @@ func (*scriptedToolCaller) JQ() string { return "" } func installScriptedCaller(t *testing.T, caller *scriptedToolCaller) { t.Helper() - previous := deps + testseam.Protect(t, &deps) InitDeps(caller) deps.Out.w = io.Discard deps.Out.errW = io.Discard - t.Cleanup(func() { deps = previous }) } func installImmediateTiming(t *testing.T) { diff --git a/internal/helpers/remaining_unit_coverage_test.go b/internal/helpers/remaining_unit_coverage_test.go index d7c7e578..238d7f75 100644 --- a/internal/helpers/remaining_unit_coverage_test.go +++ b/internal/helpers/remaining_unit_coverage_test.go @@ -20,6 +20,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -686,8 +687,7 @@ func TestCrossPlatformCoverageRemainingCommandExecutionBranches(t *testing.T) { }) t.Run("doc version confirmed revert", func(t *testing.T) { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"versions":[{"version":3}]}`}, {text: `{}`}}} if err := runDocCoverageCommand(t, caller, "--yes", "version", "revert", "--node=node", "--version=3"); err != nil { t.Fatal(err) @@ -698,8 +698,7 @@ func TestCrossPlatformCoverageRemainingCommandExecutionBranches(t *testing.T) { }) t.Run("mail invalid parameter suggestion", func(t *testing.T) { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) caller := &coverageErrorCaller{err: &CLIError{Code: CodeMCPToolError, Message: "Invalid parameter"}} InitDeps(caller) cmd := newMailCommand() @@ -712,8 +711,7 @@ func TestCrossPlatformCoverageRemainingCommandExecutionBranches(t *testing.T) { }) t.Run("sheet style dry run", func(t *testing.T) { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) batchPath := filepath.Join(t.TempDir(), "styles.json") if err := os.WriteFile(batchPath, []byte(`[{"sheetId":"Sheet1","range":"A1:B2","fontWeight":"bold"}]`), 0o600); err != nil { t.Fatal(err) @@ -727,8 +725,7 @@ func TestCrossPlatformCoverageRemainingCommandExecutionBranches(t *testing.T) { }) t.Run("business error classification", func(t *testing.T) { - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) InitDeps(&coverageErrorCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{"success":false,"message":"bad"}`}}}}) if err := callMCPTool("business", nil); err == nil { t.Fatal("business error was not classified") diff --git a/internal/helpers/report_test.go b/internal/helpers/report_test.go index cdfdc589..2901f323 100644 --- a/internal/helpers/report_test.go +++ b/internal/helpers/report_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" ) @@ -39,8 +40,7 @@ func (*reportTestCaller) JQ() string { return "" } func installReportTestDeps(t *testing.T, caller *reportTestCaller) (*bytes.Buffer, *bytes.Buffer) { t.Helper() - previous := deps - t.Cleanup(func() { deps = previous }) + testseam.Protect(t, &deps) InitDeps(caller) out := &bytes.Buffer{} errOut := &bytes.Buffer{} diff --git a/internal/output/pretty_test.go b/internal/output/pretty_test.go index d2479203..cbb24271 100644 --- a/internal/output/pretty_test.go +++ b/internal/output/pretty_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/fatih/color" ) @@ -26,9 +27,7 @@ import ( // so we flip the flag explicitly. func forceNoColor(t *testing.T) { t.Helper() - prev := color.NoColor - color.NoColor = true - t.Cleanup(func() { color.NoColor = prev }) + testseam.Swap(t, &color.NoColor, true) } func TestPretty_SchemaListRendersProducts(t *testing.T) { diff --git a/internal/pipeline/cobra_test.go b/internal/pipeline/cobra_test.go index 78d8d5bb..918f384e 100644 --- a/internal/pipeline/cobra_test.go +++ b/internal/pipeline/cobra_test.go @@ -9,6 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" ) func TestCrossPlatformCoverageFlagInfoFromCommandIncludesLocalInheritedAndAnnotations(t *testing.T) { @@ -49,8 +51,7 @@ func TestCrossPlatformCoverageFlagInfoFromCommandIncludesLocalInheritedAndAnnota } func TestCrossPlatformCoverageRunPreParseGuardAndTraversalBranches(t *testing.T) { - previousArgs := os.Args - t.Cleanup(func() { os.Args = previousArgs }) + testseam.Protect(t, &os.Args) root := &cobra.Command{Use: "root"} root.AddCommand(&cobra.Command{Use: "flagless"}) @@ -70,8 +71,7 @@ func TestCrossPlatformCoverageRunPreParseGuardAndTraversalBranches(t *testing.T) } func TestCrossPlatformCoverageRunPreParseAppliesCorrectionsOnlyOnSuccess(t *testing.T) { - previousArgs := os.Args - t.Cleanup(func() { os.Args = previousArgs }) + testseam.Protect(t, &os.Args) buildRoot := func() (*cobra.Command, *string) { root := &cobra.Command{Use: "root", SilenceErrors: true, SilenceUsage: true} diff --git a/internal/shortcut/shortcut_test.go b/internal/shortcut/shortcut_test.go index d5458065..7cd6cf81 100644 --- a/internal/shortcut/shortcut_test.go +++ b/internal/shortcut/shortcut_test.go @@ -23,6 +23,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" ) @@ -474,9 +475,7 @@ func TestCrossPlatformCoverageBuildGroupsByService(t *testing.T) { } func TestBuiltInCommandsExcludeUserDefinedShortcuts(t *testing.T) { - previous := append([]Shortcut(nil), allShortcuts...) - t.Cleanup(func() { allShortcuts = previous }) - allShortcuts = nil + testseam.Swap(t, &allShortcuts, []Shortcut(nil)) Register( Shortcut{Service: "calendar", Command: "+builtin", Execute: noop}, Shortcut{Service: "calendar", Command: "+user", UserDefined: true, Execute: noop}, diff --git a/internal/testseam/seam.go b/internal/testseam/seam.go index 20293f9a..c3e7730b 100644 --- a/internal/testseam/seam.go +++ b/internal/testseam/seam.go @@ -36,3 +36,12 @@ func Swap[T any](t *testing.T, ptr *T, next T) { *ptr = next t.Cleanup(func() { *ptr = prev }) } + +// Protect snapshots *ptr and restores it at test end without replacing it. +// Use it when the code under test mutates the seam itself (e.g. os.Args) and +// no stub value is available up front. Same sequential-only caveat as Swap. +func Protect[T any](t *testing.T, ptr *T) { + t.Helper() + prev := *ptr + t.Cleanup(func() { *ptr = prev }) +} diff --git a/internal/testseam/seam_test.go b/internal/testseam/seam_test.go index b33ec762..f0682db2 100644 --- a/internal/testseam/seam_test.go +++ b/internal/testseam/seam_test.go @@ -45,3 +45,14 @@ func TestCrossPlatformCoverageSwapFunctionSeam(t *testing.T) { t.Fatalf("function seam not restored: %d", got) } } + +func TestCrossPlatformCoverageProtectRestores(t *testing.T) { + value := 5 + t.Run("child", func(t *testing.T) { + Protect(t, &value) + value = 99 // code under test mutates the seam + }) + if value != 5 { + t.Fatalf("Protect did not restore mutated value: %d", value) + } +} diff --git a/scripts/policy/check-schema-catalog.sh b/scripts/policy/check-schema-catalog.sh index ee3376b8..aeda2625 100755 --- a/scripts/policy/check-schema-catalog.sh +++ b/scripts/policy/check-schema-catalog.sh @@ -266,6 +266,19 @@ fi # retired bindings snapshot (loadSchemaParameterBindingSnapshot) are gone with # their JSON inputs; the bans below keep them from reappearing. +# Injection-seam swaps in tests must use internal/testseam.Swap/Protect: the +# manual save/assign/t.Cleanup-restore trio can forget the restore and leak +# process-global state into sibling tests. The testseam package implements +# the mechanism and is exempt. +manual_seam_restores="$(policy_search_go \ + 't\.Cleanup\(func\(\) *\{ *[A-Za-z_][A-Za-z0-9_.]* *= *prev(ious)? *\}\)' \ + internal | grep '_test\.go:' | grep -v '^internal/testseam/' || true)" +if [ -n "$manual_seam_restores" ]; then + printf '%s\n' 'manual seam restore found; use testseam.Swap or testseam.Protect instead:' >&2 + printf '%s\n' "$manual_seam_restores" >&2 + exit 1 +fi + # Catch the common direct eager form statically; the fresh-process tests below # additionally catch indirect or multi-line package initializers. if policy_search_production_go '^[[:space:]]*var .*=[[:space:]]*(runtimeAgentMetadata|runtimeMCPMetadata|runtimeSchemaParameterBindingData)\(' \