diff --git a/AGENTS.md b/AGENTS.md index fd0100cf..37350981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ changes must not rewrite it mechanically. - AnnotateRuntime* writers → `internal/corecmd/runtimeannotate` (framework-owned) - ContractFinal cobra store + Register → `internal/corecmd/contractfinal` (framework-owned) - homology gates → `internal/cli/homology` - - Catalog assembly / `ResolveMeta` (`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`); go:embed only for reviewed inputs → `internal/cli` root (thin re-exports of annotate/store APIs; `cli/runtimeannotate` + `cli/contractfinal` are thin re-exports only) + - Catalog assembly / `ResolveMeta` (`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`); go:embed only for reviewed inputs → `internal/cli` root (package-local aliases for annotate/store APIs live in `runtime_schema_seam.go`; the former `cli/runtimeannotate` / `cli/contractfinal` shim packages are removed — import `corecmd/*` directly) - **Hard rule**: `internal/corecmd` (and its subpackages) must **not** import any `internal/cli` package - Authoring tiers (current, not aspirational): - **Tier1** — `corecmd.New` / `NewLeafCommand` (fully managed declare + execute) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af4560b..32c7d5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ### Changed - **Hints retired; ContractDecl is the leaf Schema source** (#830) — `schema_hints/`, Manual/Schema hint overlays, and `schema_agent_metadata/` delivery are removed. Selection, safety, parameters, and interface facts declare on ProductDecl / leaf `Contract` (`corecmd.ContractDecl` + `contract.ParamDecl` / `Safety`). Authoring renamed `SchemaDecl` → `ContractDecl`; nested fields reuse `contract.*` directly. -- **Contract package seam** (#830) — types / ProductDecl live under `internal/corecmd/contract` (DTO only). Annotate writers live in `internal/corecmd/runtimeannotate`; Cobra-keyed ContractFinal store + Register live in `internal/corecmd/contractfinal`; homology gates in `internal/cli/homology`. `cli` root (and `cli/runtimeannotate` / `cli/contractfinal`) keep thin re-exports for product code; Catalog/`ResolveMeta` stay on the `cli` delivery root. `internal/corecmd` must not import any `internal/cli` package. +- **Contract package seam** (#830) — types / ProductDecl live under `internal/corecmd/contract` (DTO only). Annotate writers live in `internal/corecmd/runtimeannotate`; Cobra-keyed ContractFinal store + Register live in `internal/corecmd/contractfinal`; homology gates in `internal/cli/homology`. All packages import `corecmd/*` directly; the former `cli/runtimeannotate` / `cli/contractfinal` shim packages are removed, and the `cli` root keeps only package-local aliases (`runtime_schema_seam.go`). Catalog/`ResolveMeta` stay on the `cli` delivery root. `internal/corecmd` must not import any `internal/cli` package. - **CommandMeta cache for ResolveMeta** — production `ResolveMeta` / leaf `--help` Safety project from the runtime-assembled `SchemaRegistry` into a `map[cli_path]CommandMeta` installed during `deliverySchemaCatalog` sync.Once. Steady-state lookups are O(1); full Catalog wire maps stay deferred. Registry `Source` stamps `runtime-assembled`. ### Fixed diff --git a/docs/rfc-command-framework-convergence.md b/docs/rfc-command-framework-convergence.md index 8b8cb159..bf85ebe2 100644 --- a/docs/rfc-command-framework-convergence.md +++ b/docs/rfc-command-framework-convergence.md @@ -275,8 +275,8 @@ Definition(仅声明;不可编译) |---|---|---| | **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `ContractDecl` **数据字段**(声明证据;交付见下) | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Contract`;类型真身在 `corecmd/contract`(DTO:`SafetySpec`/`ParamDecl`/`ProductDecl`/`ContractFinalPayload`;**无** Cobra store) | | **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedContractDecl` → `corecmd/contractfinal.RegisterRuntimeContractFinal`(annotate + store;产品代码经 `cli.RegisterRuntimeContractFinal` 薄 re-export) | -| **注解 seam** | Cobra `dws.schema.*` 写入 | `internal/corecmd/runtimeannotate.AnnotateRuntime*`(框架侧;`cli` / `cli/runtimeannotate` 仅薄 re-export) | -| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`(`ResolveMeta` 自同一组装投影);go:embed 仅限 reviewed 输入(registry / MCP meta / bindings 等),不得 embed Catalog | `internal/cli` 根(交付边界);ContractFinal store 在 `corecmd/contractfinal`(`cli/contractfinal` 薄 re-export) | +| **注解 seam** | Cobra `dws.schema.*` 写入 | `internal/corecmd/runtimeannotate.AnnotateRuntime*`(框架侧;`cli` 根经 `runtime_schema_seam.go` 包内别名访问;`cli/runtimeannotate` 垫片包已删,一律直引 corecmd) | +| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;`RegisterSchemaSourceRoot` → `ResolveSchemaBuild`(`ResolveMeta` 自同一组装投影);go:embed 仅限 reviewed 输入(registry / MCP meta / bindings 等),不得 embed Catalog | `internal/cli` 根(交付边界);ContractFinal store 在 `corecmd/contractfinal`(`cli` 根经 `runtime_schema_seam.go` 包内别名访问;`cli/contractfinal` 垫片包已删) | | **执行(execute)** | 钩子不发明表面 | `Validate` / `Call` / `RunE` / `PostMount` | 依赖方向硬规则:`internal/corecmd`(含子包)**不得** import 任何 `internal/cli` 包;annotate 与 ContractFinal store 归属框架侧。 diff --git a/internal/cli/command_meta.go b/internal/cli/command_meta.go index 4dfb2d1b..7b1575e5 100644 --- a/internal/cli/command_meta.go +++ b/internal/cli/command_meta.go @@ -67,14 +67,6 @@ var ( runtimeDeliverySchemaMetaIndexLazyCount atomic.Uint64 ) -func resetMetaByCLIPathStateForTest() { - metaByCLIPathOnce = sync.Once{} - metaByCLIPath = nil - runtimeDeliverySchemaMetaIndexErr = nil - runtimeDeliverySchemaMetaIndexLazyCount.Store(0) - resetDeliverySchemaCatalogStateForTest() -} - // installDeliveryCommandMeta materializes the ResolveMeta lookup from an // assembled Catalog. Called from deliverySchemaCatalog's sync.Once so Meta // shares assembly and subsequent ResolveMeta is a plain map read. diff --git a/internal/cli/contract_final_assembly_test.go b/internal/cli/contract_final_assembly_test.go index a63b4bdb..aac07285 100644 --- a/internal/cli/contract_final_assembly_test.go +++ b/internal/cli/contract_final_assembly_test.go @@ -18,9 +18,9 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate" "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/corecmd/runtimeannotate" "github.com/spf13/cobra" ) diff --git a/internal/cli/contractfinal/reexport.go b/internal/cli/contractfinal/reexport.go deleted file mode 100644 index dbe7d997..00000000 --- a/internal/cli/contractfinal/reexport.go +++ /dev/null @@ -1,28 +0,0 @@ -// 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. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package contractfinal - -import ( - corecf "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" -) - -var ( - RegisterRuntimeContractFinal = corecf.RegisterRuntimeContractFinal - RuntimeContractFinal = corecf.RuntimeContractFinal - HasRuntimeContractFinal = corecf.HasRuntimeContractFinal - ClearRuntimeContractFinalForTest = corecf.ClearRuntimeContractFinalForTest - StoreRuntimeContractFinalRawForTest = corecf.StoreRuntimeContractFinalRawForTest - ApplyParamDecls = corecf.ApplyParamDecls - HasDeclaredOrAnnotatedConfirmation = corecf.HasDeclaredOrAnnotatedConfirmation -) diff --git a/internal/cli/fortest.go b/internal/cli/fortest.go new file mode 100644 index 00000000..f1ec38d9 --- /dev/null +++ b/internal/cli/fortest.go @@ -0,0 +1,103 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "sync" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// Test helpers for this package's lazy delivery state. Production code must +// not call these; the ForTest suffix is the boundary. Cross-package tests use +// the exported ones; in-package tests may use the unexported resetters. + +func resetDeliverySchemaCatalogStateForTest() { + runtimeDeliverySchemaCatalogOnce = sync.Once{} + runtimeDeliverySchemaCatalog = loadedSchemaCatalog{} + runtimeDeliverySchemaCatalogErr = nil + runtimeDeliverySchemaCatalogLazyCount.Store(0) + runtimeDeliverySchemaCatalogMapsOnce = sync.Once{} + runtimeDeliverySchemaCatalogMapsErr = nil +} + +func resetMetaByCLIPathStateForTest() { + metaByCLIPathOnce = sync.Once{} + metaByCLIPath = nil + runtimeDeliverySchemaMetaIndexErr = nil + runtimeDeliverySchemaMetaIndexLazyCount.Store(0) + resetDeliverySchemaCatalogStateForTest() +} + +// clearDeclaredDryRunCapabilitiesForTest resets the declared index (tests only). +func clearDeclaredDryRunCapabilitiesForTest() { + declaredDryRunCapabilities.Range(func(key, _ any) bool { + declaredDryRunCapabilities.Delete(key) + return true + }) +} + +func resetReviewedDryRunCapabilitiesLazyForTest() { + reviewedDryRunCapabilitiesLazy = struct { + once sync.Once + byCanonical map[string]contract.DryRunSpec + err error + }{} +} + +// InstallProductionSchemaAssemblyForTest installs the production +// assembleSchemaCatalogFromRoot delivery path with factory and clears lazy +// caches. Tests must Cleanup via RegisterSchemaSourceRoot(nil) or a restore +// helper that reinstalls their prior delivery stub. +func InstallProductionSchemaAssemblyForTest(factory func() *cobra.Command) { + RegisterSchemaSourceRoot(factory) + assembleDeliverySchemaCatalogFn = assembleSchemaCatalogFromRoot + resetDeliverySchemaCatalogStateForTest() +} + +// MaterializeDeliverySchemaCatalogMapsForTest exercises the lazy Catalog/Tools +// materialize path used by map-based consumers. +func MaterializeDeliverySchemaCatalogMapsForTest() (sourceHash string, err error) { + loaded, err := materializeDeliverySchemaCatalogMaps() + if err != nil { + return "", err + } + return loaded.Snapshot.SourceHash, nil +} + +// DeliverySchemaAllPayloadForTest returns schema --all through the installed +// delivery loader (catalog_hash comes from Snapshot.SourceHash). +func DeliverySchemaAllPayloadForTest() (map[string]any, error) { + return deliverySchemaAllPayload() +} + +// RestorePackageCLISchemaDeliveryForTest reinstalls the package-cli TestMain +// assembled-delivery stub after a production-assembly exercise. Outside package +// cli TestMain it clears the factory and resets lazy delivery state. +func RestorePackageCLISchemaDeliveryForTest() { + if restorePackageCLISchemaDeliveryHook != nil { + restorePackageCLISchemaDeliveryHook() + return + } + schemaSourceRootFn = nil + assembleDeliverySchemaCatalogFn = assembleSchemaCatalogFromRoot + resetDeliverySchemaCatalogStateForTest() + resetMetaByCLIPathStateForTest() +} + +// restorePackageCLISchemaDeliveryHook is installed by package-cli TestMain so +// external cli_test helpers can restore the assembled-delivery stub. +var restorePackageCLISchemaDeliveryHook func() diff --git a/internal/cli/homology/final_consistency_test.go b/internal/cli/homology/final_consistency_test.go index f996ad84..56829293 100644 --- a/internal/cli/homology/final_consistency_test.go +++ b/internal/cli/homology/final_consistency_test.go @@ -14,8 +14,8 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" ) // TestAllCommandsContractFinalConsistentWithLiveCatalog walks every reviewed diff --git a/internal/cli/homology/final_lookup_test.go b/internal/cli/homology/final_lookup_test.go index e92a71af..7b846032 100644 --- a/internal/cli/homology/final_lookup_test.go +++ b/internal/cli/homology/final_lookup_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" ) func TestContractFinalFoundForHrbrain(t *testing.T) { diff --git a/internal/cli/homology/safety_homology_test.go b/internal/cli/homology/safety_homology_test.go index f6b712ac..7d86d38d 100644 --- a/internal/cli/homology/safety_homology_test.go +++ b/internal/cli/homology/safety_homology_test.go @@ -15,8 +15,8 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "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/helpers" ) diff --git a/internal/cli/runtime_schema.go b/internal/cli/runtime_schema.go index 3c82f3a7..824c96a7 100644 --- a/internal/cli/runtime_schema.go +++ b/internal/cli/runtime_schema.go @@ -25,8 +25,8 @@ import ( "sync" "sync/atomic" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/internal/cli/runtime_schema_coverage_more_test.go b/internal/cli/runtime_schema_coverage_more_test.go index cbf6d25d..db542de9 100644 --- a/internal/cli/runtime_schema_coverage_more_test.go +++ b/internal/cli/runtime_schema_coverage_more_test.go @@ -10,8 +10,9 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -48,25 +49,18 @@ func TestCrossPlatformCoverageRuntimeSchemaLoaderAndAnnotationEdges(t *testing.T } func TestCrossPlatformCoverageCollectRuntimeSchemaEntriesErrorsAndOrdering(t *testing.T) { - originalValidate := bindValidateParameterBindings - originalRegistry := loadReviewedCommandRegistry - t.Cleanup(func() { - bindValidateParameterBindings = originalValidate - loadReviewedCommandRegistry = originalRegistry - }) - - bindValidateParameterBindings = func() error { return errors.New("bindings failed") } + testseam.Swap(t, &bindValidateParameterBindings, func() error { return errors.New("bindings failed") }) if _, err := collectRuntimeSchemaEntries(&cobra.Command{Use: "dws"}); err == nil || !strings.Contains(err.Error(), "bindings failed") { t.Fatalf("validation error = %v", err) } - bindValidateParameterBindings = func() error { return nil } - loadReviewedCommandRegistry = func() (CommandRegistry, error) { + testseam.Swap(t, &bindValidateParameterBindings, func() error { return nil }) + testseam.Swap(t, &loadReviewedCommandRegistry, func() (CommandRegistry, error) { return CommandRegistry{Commands: []CommandSpec{{ CanonicalPath: "sample.run", PrimaryCLIPath: "sample run", Visibility: SchemaVisibilityPublic, Source: "reviewed_registry", ReviewReason: "test binding failure", }}}, nil - } + }) if _, err := collectRuntimeSchemaEntries(&cobra.Command{Use: "dws"}); err == nil { t.Fatal("missing Cobra path should fail binding") } @@ -196,58 +190,50 @@ func TestCrossPlatformCoverageRuntimeCommandParameterErrorEdges(t *testing.T) { cmd.Flags().String("value", "", "value") flag := cmd.Flags().Lookup("value") - originalBindingData := schemaParameterBindingData - originalResolver := resolveRuntimeSchemaField - originalPayloadSpecs := runtimeCommandParameterSpecsForPayload - t.Cleanup(func() { - schemaParameterBindingData = originalBindingData - resolveRuntimeSchemaField = originalResolver - runtimeCommandParameterSpecsForPayload = originalPayloadSpecs - }) - if specs, err := runtimeCommandParameterSpecs(nil, "sample.run", nil, RuntimeSchemaConstraints{}); err != nil || specs != nil { t.Fatalf("nil command specs = %#v, err = %v", specs, err) } - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{}, errors.New("load failed") - } + }) if _, err := runtimeCommandParameterSpecs(cmd, "sample.run", nil, RuntimeSchemaConstraints{}); err == nil || !strings.Contains(err.Error(), "load failed") { t.Fatalf("binding load error = %v", err) } - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{Bindings: map[string]map[string]string{}, MappingExclusions: map[string]string{}}, nil - } + }) if _, _, err := runtimeSchemaParameterMappingCandidates(schemaParameterBindingSnapshot{ Bindings: map[string]map[string]string{}, MappingExclusions: map[string]string{"sample.run --value": " "}, }, "sample.run", "value"); err == nil { t.Fatal("empty mapping exclusion reason should fail") } - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{ Bindings: map[string]map[string]string{}, MappingExclusions: map[string]string{"sample.run --value": " "}, }, nil - } + }) if _, err := runtimeCommandParameterSpecs(cmd, "sample.run", nil, RuntimeSchemaConstraints{}); err == nil || !strings.Contains(err.Error(), "mapping exclusion") { t.Fatalf("mapping exclusion error = %v", err) } - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{Bindings: map[string]map[string]string{}, MappingExclusions: map[string]string{}}, nil - } + }) + realResolver := resolveRuntimeSchemaField for _, target := range []string{"property", "interface_type", "description", "required", "required_when", "format", "enum", "example"} { - resolveRuntimeSchemaField = func(field string, candidates ...runtimeSchemaFieldCandidate) (runtimeSchemaFieldCandidate, error) { + testseam.Swap(t, &resolveRuntimeSchemaField, func(field string, candidates ...runtimeSchemaFieldCandidate) (runtimeSchemaFieldCandidate, error) { if field == target { return runtimeSchemaFieldCandidate{}, errors.New("forced " + target) } return resolveRuntimeSchemaCandidate(field, candidates...) - } + }) if _, err := runtimeCommandParameterSpecs(cmd, "sample.run", nil, RuntimeSchemaConstraints{}); err == nil || !strings.Contains(err.Error(), target) { t.Fatalf("%s resolution error = %v", target, err) } } - resolveRuntimeSchemaField = originalResolver + resolveRuntimeSchemaField = realResolver if specs, err := runtimeCommandParameterSpecs(&cobra.Command{Use: "empty"}, "sample.empty", nil, RuntimeSchemaConstraints{}); err != nil || specs != nil { t.Fatalf("empty specs = %#v, err = %v", specs, err) @@ -255,9 +241,9 @@ func TestCrossPlatformCoverageRuntimeCommandParameterErrorEdges(t *testing.T) { if payload, err := runtimeCommandParameters(nil, "", nil, RuntimeSchemaConstraints{}); err != nil || payload != nil { t.Fatalf("empty payload = %#v, err = %v", payload, err) } - runtimeCommandParameterSpecsForPayload = func(*cobra.Command, string, map[string]embeddedMCPParamMeta, RuntimeSchemaConstraints) ([]ParameterSpec, error) { + testseam.Swap(t, &runtimeCommandParameterSpecsForPayload, func(*cobra.Command, string, map[string]embeddedMCPParamMeta, RuntimeSchemaConstraints) ([]ParameterSpec, error) { return []ParameterSpec{{Name: "bad", Example: json.RawMessage("{")}}, nil - } + }) if _, err := runtimeCommandParameters(cmd, "sample.run", nil, RuntimeSchemaConstraints{}); err == nil || !strings.Contains(err.Error(), "serialize Schema parameter") { t.Fatalf("payload serialization error = %v", err) } diff --git a/internal/cli/runtime_schema_seam.go b/internal/cli/runtime_schema_seam.go index 804c0ce9..260d5954 100644 --- a/internal/cli/runtime_schema_seam.go +++ b/internal/cli/runtime_schema_seam.go @@ -17,15 +17,15 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate" "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/corecmd/runtimeannotate" ) -// Thin re-exports so helpers / app / shortcut keep the cli. prefix while the -// implementations live under internal/corecmd/{runtimeannotate,contractfinal}. -// Framework code imports those packages directly; this delivery root and the -// cli/* thin re-export subpackages only consume them. +// The declaration model and its runtime stores live under internal/corecmd/* +// and every package imports them directly from there. This file only keeps +// the cli.* convenience aliases so the delivery root's own code reads flat; +// no separate cli shim packages exist. type RuntimeSchemaConstraints = runtimeannotate.RuntimeSchemaConstraints diff --git a/internal/cli/runtimeannotate/reexport.go b/internal/cli/runtimeannotate/reexport.go deleted file mode 100644 index 32a60cae..00000000 --- a/internal/cli/runtimeannotate/reexport.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package runtimeannotate - -import ( - coreann "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" -) - -const ( - AnnotationProduct = coreann.AnnotationProduct - AnnotationTool = coreann.AnnotationTool - AnnotationSource = coreann.AnnotationSource - AnnotationTitle = coreann.AnnotationTitle - AnnotationDescription = coreann.AnnotationDescription - AnnotationMetaSource = coreann.AnnotationMetaSource - AnnotationExclude = coreann.AnnotationExclude - AnnotationConstraints = coreann.AnnotationConstraints - AnnotationPositionals = coreann.AnnotationPositionals - AnnotationContract = coreann.AnnotationContract - AnnotationRisk = coreann.AnnotationRisk - AnnotationRuntimeGate = coreann.AnnotationRuntimeGate - AnnotationFlagProperty = coreann.AnnotationFlagProperty - AnnotationFlagType = coreann.AnnotationFlagType - AnnotationFlagRequired = coreann.AnnotationFlagRequired - AnnotationFlagReqWhen = coreann.AnnotationFlagReqWhen - AnnotationFlagExample = coreann.AnnotationFlagExample - AnnotationFlagFormat = coreann.AnnotationFlagFormat - AnnotationFlagEnum = coreann.AnnotationFlagEnum -) - -type RuntimeSchemaConstraints = coreann.RuntimeSchemaConstraints - -var ( - AttachRuntimeSchema = coreann.AttachRuntimeSchema - AnnotateRuntimeToolMetadata = coreann.AnnotateRuntimeToolMetadata - AnnotateRuntimeFlag = coreann.AnnotateRuntimeFlag - AnnotateRuntimeFlagProperty = coreann.AnnotateRuntimeFlagProperty - AnnotateRuntimeRequiredFlags = coreann.AnnotateRuntimeRequiredFlags - AnnotateRuntimeFlagRequiredValue = coreann.AnnotateRuntimeFlagRequiredValue - AnnotateRuntimeFlagDescription = coreann.AnnotateRuntimeFlagDescription - AnnotateRuntimeFlagRequiredWhen = coreann.AnnotateRuntimeFlagRequiredWhen - AnnotateRuntimeFlagFormat = coreann.AnnotateRuntimeFlagFormat - AnnotateRuntimeFlagInterfaceType = coreann.AnnotateRuntimeFlagInterfaceType - AnnotateRuntimeFlagEnum = coreann.AnnotateRuntimeFlagEnum - AnnotateRuntimeFlagExample = coreann.AnnotateRuntimeFlagExample - AnnotateRuntimeContract = coreann.AnnotateRuntimeContract - AnnotateRuntimeRisk = coreann.AnnotateRuntimeRisk - AnnotateRuntimeGate = coreann.AnnotateRuntimeGate - RuntimeContractRisk = coreann.RuntimeContractRisk - RuntimeContractGate = coreann.RuntimeContractGate - AnnotateRuntimeConstraints = coreann.AnnotateRuntimeConstraints - CommandConstraints = coreann.CommandConstraints - NormalizeConstraints = coreann.NormalizeConstraints - ConstraintsEmpty = coreann.ConstraintsEmpty - AnnotateRuntimePositionals = coreann.AnnotateRuntimePositionals - CommandPositionals = coreann.CommandPositionals - ExcludeFromRuntimeSchema = coreann.ExcludeFromRuntimeSchema - CommandFlag = coreann.CommandFlag - SetCommandAnnotation = coreann.SetCommandAnnotation - SetFlagAnnotation = coreann.SetFlagAnnotation - SetFlagAnnotationValues = coreann.SetFlagAnnotationValues -) diff --git a/internal/cli/schema_agent_examples.go b/internal/cli/schema_agent_examples.go index 2d5d7f2a..c5bca55f 100644 --- a/internal/cli/schema_agent_examples.go +++ b/internal/cli/schema_agent_examples.go @@ -18,8 +18,8 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/internal/cli/schema_agent_selection.go b/internal/cli/schema_agent_selection.go index 33736bf7..c15062b0 100644 --- a/internal/cli/schema_agent_selection.go +++ b/internal/cli/schema_agent_selection.go @@ -21,7 +21,7 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/cli/schema_cobra_binding.go b/internal/cli/schema_cobra_binding.go index 3bd0ab36..88dbc6e9 100644 --- a/internal/cli/schema_cobra_binding.go +++ b/internal/cli/schema_cobra_binding.go @@ -12,8 +12,8 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/internal/cli/schema_cross_platform_coverage_final_test.go b/internal/cli/schema_cross_platform_coverage_final_test.go index b7b92483..3b3ad141 100644 --- a/internal/cli/schema_cross_platform_coverage_final_test.go +++ b/internal/cli/schema_cross_platform_coverage_final_test.go @@ -13,6 +13,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" ) @@ -220,9 +221,7 @@ func TestCrossPlatformCoverageFinalChangedStatementGaps(t *testing.T) { }) t.Run("validateSchemaRegistryAgentMetadata missing summary", func(t *testing.T) { - prev := finalSchemaAgentMetadata - finalSchemaAgentMetadata = func() agentMetadata { return agentMetadata{} } - t.Cleanup(func() { finalSchemaAgentMetadata = prev }) + testseam.Swap(t, &finalSchemaAgentMetadata, func() agentMetadata { return agentMetadata{} }) registry := SchemaRegistry{Products: []ProductSpec{{ ID: "sample", Tools: []ToolSpec{{ @@ -336,9 +335,7 @@ func TestCrossPlatformCoverageDeliverySchemaPayloadAndResolveMetaFactory(t *test }) t.Run("NewSchemaCommand catalog load failure", func(t *testing.T) { - original := schemaCommandCatalogError - t.Cleanup(func() { schemaCommandCatalogError = original }) - schemaCommandCatalogError = func() error { return errors.New("catalog broken") } + testseam.Swap(t, &schemaCommandCatalogError, func() error { return errors.New("catalog broken") }) cmd := NewSchemaCommand(nil) cmd.SetArgs([]string{"--all"}) if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "load typed Schema registry") { diff --git a/internal/cli/schema_cross_platform_coverage_remaining_test.go b/internal/cli/schema_cross_platform_coverage_remaining_test.go index c791a819..5ea7a680 100644 --- a/internal/cli/schema_cross_platform_coverage_remaining_test.go +++ b/internal/cli/schema_cross_platform_coverage_remaining_test.go @@ -9,8 +9,8 @@ import ( "testing" "testing/fstest" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/internal/cli/schema_cross_platform_coverage_test.go b/internal/cli/schema_cross_platform_coverage_test.go index f0fb5419..b841f794 100644 --- a/internal/cli/schema_cross_platform_coverage_test.go +++ b/internal/cli/schema_cross_platform_coverage_test.go @@ -10,8 +10,8 @@ import ( "testing" "testing/fstest" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" "github.com/spf13/pflag" ) diff --git a/internal/cli/schema_dry_run_capabilities.go b/internal/cli/schema_dry_run_capabilities.go index 71fb145d..551ad9d5 100644 --- a/internal/cli/schema_dry_run_capabilities.go +++ b/internal/cli/schema_dry_run_capabilities.go @@ -57,22 +57,6 @@ func recordDeclaredDryRunCapability(canonical string, spec contract.DryRunSpec) declaredDryRunCapabilities.Store(canonical, spec) } -// clearDeclaredDryRunCapabilitiesForTest resets the declared index (tests only). -func clearDeclaredDryRunCapabilitiesForTest() { - declaredDryRunCapabilities.Range(func(key, _ any) bool { - declaredDryRunCapabilities.Delete(key) - return true - }) -} - -func resetReviewedDryRunCapabilitiesLazyForTest() { - reviewedDryRunCapabilitiesLazy = struct { - once sync.Once - byCanonical map[string]contract.DryRunSpec - err error - }{} -} - func setReviewedDryRunCapabilityGroupsForTest(groups []dryRunCapabilityGroup) func() { previous := reviewedDryRunCapabilityGroups reviewedDryRunCapabilityGroups = groups diff --git a/internal/cli/schema_overall_gap_coverage_test.go b/internal/cli/schema_overall_gap_coverage_test.go index 19e3fe5d..8edbb5fd 100644 --- a/internal/cli/schema_overall_gap_coverage_test.go +++ b/internal/cli/schema_overall_gap_coverage_test.go @@ -11,6 +11,7 @@ import ( "testing" "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" ) @@ -704,9 +705,8 @@ func TestOverallCoverageGapSchemaCommandAndFieldResolve(t *testing.T) { if err := cmd.Execute(); err == nil { t.Fatal("--all with path must fail") } - prevCatalog := schemaCommandCatalogError - t.Cleanup(func() { schemaCommandCatalogError = prevCatalog }) - schemaCommandCatalogError = func() error { return fmt.Errorf("catalog boom") } + realCatalogError := schemaCommandCatalogError + testseam.Swap(t, &schemaCommandCatalogError, func() error { return fmt.Errorf("catalog boom") }) cmd = NewSchemaCommand(nil) cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) @@ -728,7 +728,7 @@ func TestOverallCoverageGapSchemaCommandAndFieldResolve(t *testing.T) { t.Fatalf("review-reason tie-break = %#v err=%v", winner, err) } - schemaCommandCatalogError = prevCatalog + schemaCommandCatalogError = realCatalogError // Delivery is installed by TestMain; exercise success + compact branches. for _, args := range [][]string{ {}, @@ -946,9 +946,7 @@ func TestOverallCoverageGapDeliveryCompletenessAndDryRun(t *testing.T) { if err != nil { t.Fatal(err) } - prevLoad := loadReviewedCommandRegistry - t.Cleanup(func() { loadReviewedCommandRegistry = prevLoad }) - loadReviewedCommandRegistry = func() (CommandRegistry, error) { return CommandRegistry{}, fmt.Errorf("embed boom") } + testseam.Swap(t, &loadReviewedCommandRegistry, func() (CommandRegistry, error) { return CommandRegistry{}, fmt.Errorf("embed boom") }) if _, err := ValidateCommandRegistrySource(merged); err == nil || !strings.Contains(err.Error(), "embed boom") { t.Fatalf("embed load error = %v", err) } @@ -958,16 +956,14 @@ func TestOverallCoverageGapDeliveryCompletenessAndDryRun(t *testing.T) { if err != nil { t.Fatal(err) } - loadReviewedCommandRegistry = func() (CommandRegistry, error) { return alt, nil } + testseam.Swap(t, &loadReviewedCommandRegistry, func() (CommandRegistry, error) { return alt, nil }) if _, err := ValidateCommandRegistrySource(merged); err == nil || !strings.Contains(err.Error(), "disagrees") { t.Fatalf("semantic disagree error = %v", err) } - prevBind := schemaParameterBindingData - t.Cleanup(func() { schemaParameterBindingData = prevBind }) - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{Bindings: map[string]map[string]string{"a.b": {"flag": "prop"}}}, nil - } + }) if got, err := LoadSchemaParameterBindings(); err != nil || got["a.b"]["flag"] != "prop" { t.Fatalf("LoadSchemaParameterBindings() = %#v err=%v", got, err) } @@ -1065,20 +1061,18 @@ func TestOverallCoverageGapRuntimeParamsAndAgentMetadata(t *testing.T) { t.Fatal("ghost group product miss must surface") } - prevMeta := finalSchemaAgentMetadata - t.Cleanup(func() { finalSchemaAgentMetadata = prevMeta }) - finalSchemaAgentMetadata = func() agentMetadata { + testseam.Swap(t, &finalSchemaAgentMetadata, func() agentMetadata { return agentMetadata{Tools: map[string]agentToolMetadata{ "sample.group.run": {}, "sample group run": {}, }} - } + }) if err := validateSchemaRegistryAgentMetadata(registry); err == nil || !strings.Contains(err.Error(), "both resolve") { t.Fatalf("duplicate agent metadata keys error = %v", err) } - finalSchemaAgentMetadata = func() agentMetadata { + testseam.Swap(t, &finalSchemaAgentMetadata, func() agentMetadata { return agentMetadata{Tools: map[string]agentToolMetadata{"sample.group.run": {}}} - } + }) if err := validateSchemaRegistryAgentMetadata(registry); err != nil { t.Fatalf("matching agent metadata should pass: %v", err) } diff --git a/internal/cli/schema_parameter_bindings_coverage_more_test.go b/internal/cli/schema_parameter_bindings_coverage_more_test.go index 3ac7fbc7..0ee411c9 100644 --- a/internal/cli/schema_parameter_bindings_coverage_more_test.go +++ b/internal/cli/schema_parameter_bindings_coverage_more_test.go @@ -9,6 +9,7 @@ import ( "testing" "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" ) @@ -236,11 +237,9 @@ func TestCrossPlatformCoverageSchemaParameterBindingHelpersAndLoaderErrors(t *te t.Fatalf("binding annotation = %#v", got) } - previous := schemaParameterBindingData - schemaParameterBindingData = func() (schemaParameterBindingSnapshot, error) { + testseam.Swap(t, &schemaParameterBindingData, func() (schemaParameterBindingSnapshot, error) { return schemaParameterBindingSnapshot{}, errors.New("binding data failed") - } - t.Cleanup(func() { schemaParameterBindingData = previous }) + }) if err := ValidateSchemaParameterBindings(); err == nil || !strings.Contains(err.Error(), "binding data failed") { t.Fatalf("ValidateSchemaParameterBindings() error = %v", err) } diff --git a/internal/cli/schema_platform_gap_coverage_test.go b/internal/cli/schema_platform_gap_coverage_test.go index ced753e4..0a679dba 100644 --- a/internal/cli/schema_platform_gap_coverage_test.go +++ b/internal/cli/schema_platform_gap_coverage_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/cli/schema_registry_test_helpers_test.go b/internal/cli/schema_registry_test_helpers_test.go index 32128547..592d54c6 100644 --- a/internal/cli/schema_registry_test_helpers_test.go +++ b/internal/cli/schema_registry_test_helpers_test.go @@ -8,7 +8,7 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/cli/schema_runtime_registry.go b/internal/cli/schema_runtime_registry.go index 22d74a53..60e90002 100644 --- a/internal/cli/schema_runtime_registry.go +++ b/internal/cli/schema_runtime_registry.go @@ -9,8 +9,8 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "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/spf13/cobra" ) diff --git a/internal/cli/schema_runtime_registry_contract_test.go b/internal/cli/schema_runtime_registry_contract_test.go index f0434bf4..60a10acb 100644 --- a/internal/cli/schema_runtime_registry_contract_test.go +++ b/internal/cli/schema_runtime_registry_contract_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/cli/schema_source_root.go b/internal/cli/schema_source_root.go index 25c62f42..314cda9d 100644 --- a/internal/cli/schema_source_root.go +++ b/internal/cli/schema_source_root.go @@ -58,15 +58,6 @@ func SchemaSourceRootRegistered() bool { return schemaSourceRootFn != nil } -func resetDeliverySchemaCatalogStateForTest() { - runtimeDeliverySchemaCatalogOnce = sync.Once{} - runtimeDeliverySchemaCatalog = loadedSchemaCatalog{} - runtimeDeliverySchemaCatalogErr = nil - runtimeDeliverySchemaCatalogLazyCount.Store(0) - runtimeDeliverySchemaCatalogMapsOnce = sync.Once{} - runtimeDeliverySchemaCatalogMapsErr = nil -} - var ( assembleDeliverySchemaCatalogFn = assembleSchemaCatalogFromRoot resolveSchemaBuildForDelivery = ResolveSchemaBuild @@ -120,50 +111,6 @@ func assembleSchemaCatalogFromRoot(root *cobra.Command) (loadedSchemaCatalog, er }, nil } -// InstallProductionSchemaAssemblyForTest installs the production -// assembleSchemaCatalogFromRoot delivery path with factory and clears lazy -// caches. Tests must Cleanup via RegisterSchemaSourceRoot(nil) or a restore -// helper that reinstalls their prior delivery stub. -func InstallProductionSchemaAssemblyForTest(factory func() *cobra.Command) { - RegisterSchemaSourceRoot(factory) - assembleDeliverySchemaCatalogFn = assembleSchemaCatalogFromRoot - resetDeliverySchemaCatalogStateForTest() -} - -// MaterializeDeliverySchemaCatalogMapsForTest exercises the lazy Catalog/Tools -// materialize path used by map-based consumers. -func MaterializeDeliverySchemaCatalogMapsForTest() (sourceHash string, err error) { - loaded, err := materializeDeliverySchemaCatalogMaps() - if err != nil { - return "", err - } - return loaded.Snapshot.SourceHash, nil -} - -// DeliverySchemaAllPayloadForTest returns schema --all through the installed -// delivery loader (catalog_hash comes from Snapshot.SourceHash). -func DeliverySchemaAllPayloadForTest() (map[string]any, error) { - return deliverySchemaAllPayload() -} - -// RestorePackageCLISchemaDeliveryForTest reinstalls the package-cli TestMain -// assembled-delivery stub after a production-assembly exercise. Outside package -// cli TestMain it clears the factory and resets lazy delivery state. -func RestorePackageCLISchemaDeliveryForTest() { - if restorePackageCLISchemaDeliveryHook != nil { - restorePackageCLISchemaDeliveryHook() - return - } - schemaSourceRootFn = nil - assembleDeliverySchemaCatalogFn = assembleSchemaCatalogFromRoot - resetDeliverySchemaCatalogStateForTest() - resetMetaByCLIPathStateForTest() -} - -// restorePackageCLISchemaDeliveryHook is installed by package-cli TestMain so -// external cli_test helpers can restore the assembled-delivery stub. -var restorePackageCLISchemaDeliveryHook func() - // deliverySchemaCatalog is the sole production Catalog loader. It lazily // assembles via ResolveSchemaBuild and caches the ResolveMeta map. Without a // factory it fails closed. diff --git a/internal/cli/schema_source_root_coverage_test.go b/internal/cli/schema_source_root_coverage_test.go index 16b4193a..fd9af9f3 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" ) @@ -206,11 +207,9 @@ func TestCrossPlatformCoverageReviewedCommandRegistrySourceHash(t *testing.T) { if err != nil || hash == "" { t.Fatalf("ReviewedCommandRegistrySourceHash = %q err=%v", hash, err) } - prev := loadReviewedCommandRegistry - t.Cleanup(func() { loadReviewedCommandRegistry = prev }) - loadReviewedCommandRegistry = func() (CommandRegistry, error) { + testseam.Swap(t, &loadReviewedCommandRegistry, func() (CommandRegistry, error) { return CommandRegistry{}, fmt.Errorf("load boom") - } + }) if _, err := ReviewedCommandRegistrySourceHash(); err == nil || !strings.Contains(err.Error(), "load boom") { t.Fatalf("ReviewedCommandRegistrySourceHash error = %v", err) } diff --git a/internal/corecmd/contract/fortest.go b/internal/corecmd/contract/fortest.go new file mode 100644 index 00000000..a84f24b7 --- /dev/null +++ b/internal/corecmd/contract/fortest.go @@ -0,0 +1,35 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package contract + +import "strings" + +// Cross-package test helpers for this package's global registration store. +// Production code must not call these; the ForTest suffix is the boundary. + +// ClearProductDeclForTest removes a registration (tests only). +func ClearProductDeclForTest(productID string) { + productID = strings.TrimSpace(productID) + if productID != "" { + productDecls.Delete(productID) + } +} + +// StoreProductDeclRawForTest stores an arbitrary map value (tests only). +func StoreProductDeclRawForTest(productID string, value any) { + productID = strings.TrimSpace(productID) + if productID != "" { + productDecls.Store(productID, value) + } +} diff --git a/internal/corecmd/contract/product.go b/internal/corecmd/contract/product.go index 94cf6095..50b637f8 100644 --- a/internal/corecmd/contract/product.go +++ b/internal/corecmd/contract/product.go @@ -111,22 +111,6 @@ func RegisteredProductDeclIDs() []string { return ids } -// ClearProductDeclForTest removes a registration (tests only). -func ClearProductDeclForTest(productID string) { - productID = strings.TrimSpace(productID) - if productID != "" { - productDecls.Delete(productID) - } -} - -// StoreProductDeclRawForTest stores an arbitrary map value (tests only). -func StoreProductDeclRawForTest(productID string, value any) { - productID = strings.TrimSpace(productID) - if productID != "" { - productDecls.Store(productID, value) - } -} - // ProductSelectionFromDecl projects a ProductDecl into SelectionSpec plus // contract_final FieldProvenance for ProductSpec assembly. func ProductSelectionFromDecl(decl ProductDecl) (SelectionSpec, map[string]FieldProvenance) { diff --git a/internal/cli/contractfinal/doc.go b/internal/corecmd/contractfinal/fortest.go similarity index 51% rename from internal/cli/contractfinal/doc.go rename to internal/corecmd/contractfinal/fortest.go index f72d78c5..2a2899ba 100644 --- a/internal/cli/contractfinal/doc.go +++ b/internal/corecmd/contractfinal/fortest.go @@ -11,11 +11,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package contractfinal is a thin delivery-facing re-export of -// internal/corecmd/contractfinal. -// -// Ownership of the Cobra-keyed ContractFinal store and Register seam lives -// under the command framework. Production product code should prefer -// cli.RegisterRuntimeContractFinal; framework code calls -// corecmd/contractfinal.RegisterRuntimeContractFinal directly. package contractfinal + +import "github.com/spf13/cobra" + +// Cross-package test helpers for this package's command-keyed store. +// Production code must not call these; the ForTest suffix is the boundary. + +// ClearRuntimeContractFinalForTest removes a registration (tests only). +func ClearRuntimeContractFinalForTest(cmd *cobra.Command) { + if cmd != nil { + contractFinalByCommand.Delete(cmd) + } +} + +// StoreRuntimeContractFinalRawForTest injects a raw map value (tests only). +func StoreRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) { + if cmd != nil { + contractFinalByCommand.Store(cmd, raw) + } +} diff --git a/internal/corecmd/contractfinal/store.go b/internal/corecmd/contractfinal/store.go index ded230e3..fe31376f 100644 --- a/internal/corecmd/contractfinal/store.go +++ b/internal/corecmd/contractfinal/store.go @@ -63,17 +63,3 @@ func HasRuntimeContractFinal(cmd *cobra.Command) bool { _, ok := contractFinalByCommand.Load(cmd) return ok } - -// ClearRuntimeContractFinalForTest removes a registration (tests only). -func ClearRuntimeContractFinalForTest(cmd *cobra.Command) { - if cmd != nil { - contractFinalByCommand.Delete(cmd) - } -} - -// StoreRuntimeContractFinalRawForTest injects a raw map value (tests only). -func StoreRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) { - if cmd != nil { - contractFinalByCommand.Store(cmd, raw) - } -} diff --git a/internal/corecmd/corecmd_test.go b/internal/corecmd/corecmd_test.go index 02fed7f8..3f157d95 100644 --- a/internal/corecmd/corecmd_test.go +++ b/internal/corecmd/corecmd_test.go @@ -26,6 +26,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/spf13/cobra" ) @@ -1549,9 +1550,7 @@ func TestCrossPlatformCoverageConfirmSafetyTerminalPromptsBeforeReading(t *testi } func TestCrossPlatformCoverageConfirmSafetyTerminalProbeHook(t *testing.T) { - prev := stdinIsTerminalFn - t.Cleanup(func() { stdinIsTerminalFn = prev }) - stdinIsTerminalFn = func(io.Reader) bool { return true } + testseam.Swap(t, &stdinIsTerminalFn, func(io.Reader) bool { return true }) var stderr strings.Builder cmd := newTestCommand() diff --git a/internal/corecmd/runtimeannotate/doc.go b/internal/corecmd/runtimeannotate/doc.go index 8678e1a5..4869c902 100644 --- a/internal/corecmd/runtimeannotate/doc.go +++ b/internal/corecmd/runtimeannotate/doc.go @@ -25,6 +25,6 @@ // // Dependency direction: corecmd and corecmd/contractfinal import this package. // This package must never import internal/cli (root or subpackages). -// Product / delivery code may consume via cli root thin re-exports or -// internal/cli/runtimeannotate (thin re-export of this package). +// Product / delivery code consumes this package directly, or via the cli +// root's package-local aliases (internal/cli/runtime_schema_seam.go). package runtimeannotate diff --git a/internal/generator/agentmetadata/contract_final.go b/internal/generator/agentmetadata/contract_final.go index 1a63cfe2..f3ceb627 100644 --- a/internal/generator/agentmetadata/contract_final.go +++ b/internal/generator/agentmetadata/contract_final.go @@ -18,8 +18,8 @@ import ( "sort" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/generator/agentmetadata/contract_final_coverage_test.go b/internal/generator/agentmetadata/contract_final_coverage_test.go index 042f208f..b1ab24be 100644 --- a/internal/generator/agentmetadata/contract_final_coverage_test.go +++ b/internal/generator/agentmetadata/contract_final_coverage_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go index 583d1375..dc2121f6 100644 --- a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go +++ b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go @@ -12,8 +12,9 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "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" ) @@ -337,60 +338,52 @@ func TestCrossPlatformCoverageMetadataSourceAndPathEdges(t *testing.T) { if _, _, _, err := GenerateFromCommandRoot(".", nil, Options{}); err == nil || !strings.Contains(err.Error(), "schema source root is nil") { t.Fatalf("GenerateFromCommandRoot nil root = %v", err) } - prevBuild := pipelineBuildEffectiveRegistry - prevBind := pipelineBindEffectiveRegistry - prevGenerate := pipelineGenerateMetadata - t.Cleanup(func() { - pipelineBuildEffectiveRegistry = prevBuild - pipelineBindEffectiveRegistry = prevBind - pipelineGenerateMetadata = prevGenerate - }) - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { + testseam.Swap(t, &pipelineBuildEffectiveRegistry, func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { return cli.EffectiveCommandRegistry{}, nil - } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { + }) + testseam.Swap(t, &pipelineBindEffectiveRegistry, func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { return cli.BoundCommandRegistry{}, nil - } - pipelineGenerateMetadata = func(Options) (File, Stats, error) { + }) + testseam.Swap(t, &pipelineGenerateMetadata, func(Options) (File, Stats, error) { return File{}, Stats{}, nil - } + }) if _, _, _, err := GenerateFromCommandRoot(" ", &cobra.Command{Use: "dws"}, Options{}); err != nil { t.Fatalf("GenerateFromCommandRoot empty rootPath defaults: %v", err) } - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { + testseam.Swap(t, &pipelineBuildEffectiveRegistry, func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { return cli.EffectiveCommandRegistry{}, errors.New("build boom") - } + }) if _, _, _, err := GenerateFromCommandRoot(".", &cobra.Command{Use: "dws"}, Options{}); err == nil || !strings.Contains(err.Error(), "build effective") { t.Fatalf("build error = %v", err) } - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { + testseam.Swap(t, &pipelineBuildEffectiveRegistry, func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { return cli.EffectiveCommandRegistry{}, nil - } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { + }) + testseam.Swap(t, &pipelineBindEffectiveRegistry, func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { return cli.BoundCommandRegistry{}, errors.New("bind boom") - } + }) if _, _, _, err := GenerateFromCommandRoot(".", &cobra.Command{Use: "dws"}, Options{}); err == nil || !strings.Contains(err.Error(), "bind effective") { t.Fatalf("bind error = %v", err) } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { + testseam.Swap(t, &pipelineBindEffectiveRegistry, func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { return cli.BoundCommandRegistry{}, nil - } - pipelineGenerateMetadata = func(Options) (File, Stats, error) { + }) + testseam.Swap(t, &pipelineGenerateMetadata, func(Options) (File, Stats, error) { return File{}, Stats{}, errors.New("generate boom") - } + }) if _, _, _, err := GenerateFromCommandRoot(".", &cobra.Command{Use: "dws"}, Options{}); err == nil || !strings.Contains(err.Error(), "generate in-memory") { t.Fatalf("generate error = %v", err) } - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { + testseam.Swap(t, &pipelineBuildEffectiveRegistry, func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { return cli.EffectiveCommandRegistry{Commands: []cli.CommandSpec{{ CanonicalPath: "coverage.missing", PrimaryCLIPath: "coverage missing", Visibility: cli.SchemaVisibilityPublic, }}}, nil - } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { + }) + testseam.Swap(t, &pipelineBindEffectiveRegistry, func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { return cli.BoundCommandRegistry{}, nil - } + }) if _, _, _, err := GenerateFromCommandRoot(".", &cobra.Command{Use: "dws"}, Options{}); err == nil || !strings.Contains(err.Error(), "selection coverage incomplete") { t.Fatalf("selection coverage error = %v", err) } diff --git a/internal/generator/agentmetadata/pipeline.go b/internal/generator/agentmetadata/pipeline.go index 5c273648..4210333c 100644 --- a/internal/generator/agentmetadata/pipeline.go +++ b/internal/generator/agentmetadata/pipeline.go @@ -19,8 +19,8 @@ import ( "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" "github.com/spf13/cobra" ) diff --git a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go index 2ff47501..6d0a97d0 100644 --- a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go +++ b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go @@ -13,8 +13,8 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "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/generator/agentmetadata" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/outputguard" "github.com/spf13/cobra" diff --git a/internal/helpers/aitable_schema_test.go b/internal/helpers/aitable_schema_test.go index 4616c889..78bbea9e 100644 --- a/internal/helpers/aitable_schema_test.go +++ b/internal/helpers/aitable_schema_test.go @@ -22,8 +22,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal" ) func TestDeclareLeafMetadataRejectsExecutionSurface(t *testing.T) { diff --git a/internal/helpers/contract_defects_test.go b/internal/helpers/contract_defects_test.go index 2cfdb535..4853922c 100644 --- a/internal/helpers/contract_defects_test.go +++ b/internal/helpers/contract_defects_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" + "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/pkg/edition" "github.com/spf13/cobra" diff --git a/internal/cli/runtimeannotate/doc.go b/internal/shortcut/fortest.go similarity index 61% rename from internal/cli/runtimeannotate/doc.go rename to internal/shortcut/fortest.go index 32b296e8..e22887af 100644 --- a/internal/cli/runtimeannotate/doc.go +++ b/internal/shortcut/fortest.go @@ -11,10 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package runtimeannotate is a thin delivery-facing re-export of -// internal/corecmd/runtimeannotate. -// -// Ownership of AnnotateRuntime* writers lives under the command framework. -// Prefer cli root re-exports for product code; this subpackage exists so -// existing import paths keep resolving to the same single implementation. -package runtimeannotate +package shortcut + +import "github.com/spf13/cobra" + +// Cross-package test helpers. Production code must not call these; the +// ForTest suffix is the boundary. + +// RuntimeContextForTest constructs a RuntimeContext for cross-package tests. +func RuntimeContextForTest(cmd *cobra.Command, s Shortcut) *RuntimeContext { + return &RuntimeContext{cmd: cmd, shortcut: s} +} diff --git a/internal/shortcut/runner.go b/internal/shortcut/runner.go index fae3134c..f208e667 100644 --- a/internal/shortcut/runner.go +++ b/internal/shortcut/runner.go @@ -100,11 +100,6 @@ func (rt *RuntimeContext) Changed(name string) bool { return f != nil && f.Changed } -// RuntimeContextForTest constructs a RuntimeContext for cross-package tests. -func RuntimeContextForTest(cmd *cobra.Command, s Shortcut) *RuntimeContext { - return &RuntimeContext{cmd: cmd, shortcut: s} -} - // DryRun reports whether --dry-run is set (inherited from the root command). func (rt *RuntimeContext) DryRun() bool { return globalBool(rt.cmd, "dry-run") } diff --git a/internal/shortcut/shortcut_test.go b/internal/shortcut/shortcut_test.go index 7891503a..d8aa3bb4 100644 --- a/internal/shortcut/shortcut_test.go +++ b/internal/shortcut/shortcut_test.go @@ -19,9 +19,9 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" "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/corecmd/contractfinal" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" diff --git a/internal/testseam/seam.go b/internal/testseam/seam.go new file mode 100644 index 00000000..747cab10 --- /dev/null +++ b/internal/testseam/seam.go @@ -0,0 +1,37 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testseam turns the package-var injection-seam swap into a mechanism +// instead of a convention. Tests that stub a package-level function var +// (e.g. pipelineBuildEffectiveRegistry) must use Swap so the previous value +// is structurally restored via t.Cleanup; a forgotten restore then cannot +// leak state into sibling tests. +package testseam + +import "testing" + +// Swap replaces *ptr with next for the duration of the test and restores the +// previous value when the test ends. It is the required form for package-var +// seam swaps: +// +// testseam.Swap(t, &somePackageFn, func(args) (ret, error) { ... }) +// +// Prefer it over the manual `prev := fn; t.Cleanup(...); fn = stub` trio: +// Swap cannot forget the restore, and parallel-safe call sites read the seam +// after the swap through the same pointer. +func Swap[T any](t *testing.T, ptr *T, next T) { + t.Helper() + prev := *ptr + *ptr = next + t.Cleanup(func() { *ptr = prev }) +} diff --git a/internal/testseam/seam_test.go b/internal/testseam/seam_test.go new file mode 100644 index 00000000..c5516c9b --- /dev/null +++ b/internal/testseam/seam_test.go @@ -0,0 +1,47 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testseam + +import "testing" + +func TestSwapSetsAndRestores(t *testing.T) { + value := 1 + t.Run("child", func(t *testing.T) { + Swap(t, &value, 42) + if value != 42 { + t.Fatalf("Swap did not install next value: %d", value) + } + Swap(t, &value, 43) + if value != 43 { + t.Fatalf("second Swap did not install next value: %d", value) + } + }) + if value != 1 { + t.Fatalf("Swap did not restore previous value: %d", value) + } +} + +func TestSwapFunctionSeam(t *testing.T) { + called := false + fn := func() int { return 7 } + t.Run("child", func(t *testing.T) { + Swap(t, &fn, func() int { called = true; return 9 }) + if got := fn(); got != 9 || !called { + t.Fatalf("swapped function not invoked: got=%d called=%v", got, called) + } + }) + if got := fn(); got != 7 { + t.Fatalf("function seam not restored: %d", got) + } +}