Files
dingtalk-workspace-cli/scripts/policy/check-schema-catalog.sh
玉澜 0fbdfe0130 test(seam): make testseam the only seam-swap form and legislate it
- Add testseam.Protect for save-and-restore seams with no up-front stub
  value (e.g. os.Args mutated by the code under test).
- Migrate all 35 remaining manual prev/assign/t.Cleanup-restore trios to
  testseam.Swap/Protect across app, auth, cli, event, helpers, output,
  pipeline, and shortcut tests; restores can no longer be forgotten.
- check-schema-catalog.sh: fail closed when a manual seam restore
  reappears in any *_test.go (internal/testseam exempt).

Seam injection is now a mechanism, not a convention.
2026-08-03 20:00:47 +08:00

327 lines
16 KiB
Bash
Executable File

#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
cd "$ROOT"
. "$ROOT/scripts/policy/search.sh"
. "$ROOT/scripts/policy/policy-runtime.sh"
policy_prepare_runtime "$ROOT"
# Assemble a fresh runtime Catalog dump (ResolveSchemaBuild via with-catalog.sh)
# into the single-document shape (version + surface_hash + source_hash +
# catalog + tools) that the jq queries below consume. No committed
# schema_catalog/ split is consulted.
catalog="$(mktemp)"
trap 'rm -f "$catalog"' EXIT HUP INT TERM
scripts/policy/with-catalog.sh >"$catalog"
if [ -e internal/cli/schema_native_contracts.go ] ||
[ -e internal/cli/schema_native_contracts_generated.go ] ||
policy_search_go 'ApplyNativeRuntimeSchemaContracts|nativeRuntimeSchemaContracts|runtimeSchemaIdentityCandidate' internal/cli; then
printf '%s\n' 'native Schema identity materialization must not be reintroduced' >&2
exit 1
fi
# Legacy hint maps used to select primary CLI paths and discover helper roots.
# They are identity/navigation sources, so bringing any of them back would
# silently reintroduce a second source beside the identity collector.
if policy_search_go '(schemaPrimaryCLIPath|RuntimeSchemaRootHint|RegisterRuntimeSchemaRoot|PrimaryCLIPaths|RegisterSchemaProductVisibility|SchemaProductVisibilityFor|productVisibility)' \
internal/cli; then
printf '%s\n' 'legacy Schema hint navigation or visibility sources must not be reintroduced' >&2
exit 1
fi
# The Go delivery gates compare final content against the reviewed
# CommandRegistry. This shell check intentionally treats Catalog as output and
# does not decode the registry into a competing identity model.
registry_count="$(jq -r '.tools | length' "$catalog")"
catalog_count="$registry_count"
catalog_product_count="$(jq -r '.catalog.count' "$catalog")"
# Agent metadata is no longer a shipped intermediate. Selection completeness is
# proven on the Catalog itself (every tool/product carries Agent prose).
agent_registry_count="$registry_count"
agent_product_count="$catalog_product_count"
agent_selection_count="$(jq -r '[.tools[] | select((.use_when|type)=="array" and (.avoid_when|type)=="array" and (.examples|type)=="array" and ((.interface_mode//"")|length)>0)] | length' "$catalog")"
if [ "$agent_registry_count" != "$registry_count" ] ||
[ "$agent_product_count" != "$catalog_product_count" ] ||
[ "$agent_selection_count" != "$registry_count" ]; then
printf 'generated schema counts disagree: registry=%s catalog=%s products=%s selection=%s\n' \
"$registry_count" "$catalog_count" "$catalog_product_count" \
"$agent_selection_count" >&2
exit 1
fi
# schema_mcp_metadata.json (pinned MCP baseline) is retired. Schema assembly
# is Contract/ParamDecl/Interface + Cobra only; the pin must not reappear.
if [ -e internal/cli/schema_mcp_metadata.json ]; then
printf '%s\n' 'retired schema_mcp_metadata.json must not be present' >&2
exit 1
fi
# schema_hints/ audit JSON is retired. Selection completeness, sibling
# routing, and interface disposition are proven on the Catalog itself
# (ContractFinal / ProductDecl provenance) by the jq gates below and the
# focused Go tests invoked at the end of this script.
if [ -e internal/cli/schema_hints ]; then
printf '%s\n' 'retired schema_hints/ must not be present' >&2
exit 1
fi
catalog_registry_hash="$(jq -r '.surface_hash' "$catalog")"
if [ -z "$catalog_registry_hash" ] || [ "$catalog_registry_hash" = "null" ]; then
printf 'schema Catalog is missing surface_hash\n' >&2
exit 1
fi
if ! jq -e --arg registry_count "$registry_count" '
(.tools | length) == ($registry_count | tonumber) and
all(.catalog.products[];
((.agent_summary // "") | length) > 0 and
(has("use_when") and (.use_when | type) == "array" and (.use_when | length) > 0) and
(has("avoid_when") and (.avoid_when | type) == "array" and (.avoid_when | length) > 0) and
(
(.field_provenance.agent_summary.precedence // "") == "contract_final" or
(.field_provenance.agent_summary.source // "") == "corecmd.contract" or
(.field_provenance.agent_summary.source // "") == "cli.product_decl"
)
) and
all(.tools[];
((.agent_summary // "") | length) > 0 and
(.effect == "read" or .effect == "write" or .effect == "destructive") and
(.risk == "low" or .risk == "medium" or .risk == "high") and
(.confirmation == "not_required" or .confirmation == "user_required") and
(.idempotency == "idempotent" or .idempotency == "non_idempotent" or .idempotency == "unknown") and
(has("use_when") and (.use_when | type) == "array") and
(has("avoid_when") and (.avoid_when | type) == "array") and
(has("examples") and (.examples | type) == "array") and
(.interface_mode == "mcp" or .interface_mode == "composite" or .interface_mode == "local") and
(.availability == "available" or .availability == "unavailable") and
(. as $tool | all(.examples[];
startswith("dws " + $tool.primary_cli_path) and
(test("(^|\\s)--yes(\\s|$)") | not)
)) and
(if .availability == "unavailable" then
.interface_ref == null and ((.interface_reason // "") | length) > 0
elif .interface_mode == "mcp" then
.interface_ref != null
elif .interface_mode == "local" then
.interface_ref == null
elif .interface_mode == "composite" then
.interface_ref == null and ((.interface_reason // "") | length) > 0
else false end) and
(
.field_provenance.agent_summary.source == "corecmd.contract" or
.field_provenance.agent_summary.source == "cli.product_decl" or
(.field_provenance.agent_summary.precedence // "") == "contract_final"
) and
(
(.field_provenance.interface_mode.precedence // "") == "contract_final" or
(.field_provenance.interface_mode.source // "") == "corecmd.contract"
)
)
' "$catalog" >/dev/null; then
printf '%s\n' 'schema tools must have complete Agent summary/effect/safety metadata' >&2
exit 1
fi
if ! jq -e 'all(.tools[];
if .availability == "unavailable" then .interface_ref == null
elif .interface_mode == "mcp" then .interface_ref != null
else .interface_ref == null
end
)' "$catalog" >/dev/null; then
printf '%s
' 'schema interface disposition is inconsistent with interface_ref presence' >&2
exit 1
fi
# MCP service review (schema_mcp_service_review.json and any ledger) is
# retired: no snapshot_source_hash / missing_services disposition gate.
if [ -e internal/cli/schema_mcp_service_review.json ] ||
[ -e internal/cli/schema_mcp_service_review_ledger.go ]; then
printf '%s\n' 'retired MCP service review artifact must not be present' >&2
exit 1
fi
if ! jq -e '
.tools["chat.send_personal_message"].primary_cli_path == "chat message send" and
.tools["chat.reply_personal_message"].primary_cli_path == "chat message reply" and
.tools["chat.reply_personal_message"].interface_ref == {
"product_id": "chat",
"rpc_name": "send_personal_message"
} and
(.tools | has("chat.upload_conversation_file") | not)
' "$catalog" >/dev/null; then
printf '%s\n' 'chat send/reply schema identities are inconsistent' >&2
exit 1
fi
# declare≡execute may project hidden execute-side siblings into constraint
# groups. Require every group to touch at least one published parameter/positional;
# unpublished members are allowed as hidden companions.
if ! jq -e '
[.tools[] | select(.constraints != null)] as $tools |
($tools | length) >= 21 and
all($tools[];
(((.parameters // {}) | keys) + ((.positionals // []) | map(.name))) as $names |
def ok_group:
length > 1 and
all(.[]; type == "string" and length > 0) and
any(.[]; IN($names[]));
all((.constraints.mutually_exclusive // [])[]; ok_group) and
all((.constraints.require_one_of // [])[]; ok_group) and
all((.constraints.require_together // [])[]; ok_group)
)
' "$catalog" >/dev/null; then
printf '%s\n' 'schema command constraints are incomplete or reference unknown parameters' >&2
exit 1
fi
# Track 1 Phase 2: active bindings JSON is retired. ParamDecl.Property owns
# property delivery; mapping exclusions / removals live in the Go ledger.
if [ -e internal/cli/schema_parameter_bindings.json ]; then
printf '%s\n' 'retired schema_parameter_bindings.json must not be present; use schema_parameter_mapping_ledger.go' >&2
exit 1
fi
if [ ! -f internal/cli/schema_parameter_mapping_ledger.go ]; then
printf '%s\n' 'missing reviewed parameter mapping ledger internal/cli/schema_parameter_mapping_ledger.go' >&2
exit 1
fi
if ! grep -q 'var reviewedSchemaParameterMappingExclusions = map' internal/cli/schema_parameter_mapping_ledger.go; then
printf '%s\n' 'schema_parameter_mapping_ledger.go must declare reviewedSchemaParameterMappingExclusions' >&2
exit 1
fi
if ! grep -q 'var reviewedSchemaParameterBindingRemovals = map' internal/cli/schema_parameter_mapping_ledger.go; then
printf '%s\n' 'schema_parameter_mapping_ledger.go must declare reviewedSchemaParameterBindingRemovals' >&2
exit 1
fi
if ! jq -e '
[.. | objects | select(
has("endpoint") or has("auth_headers") or has("authorization") or
has("access_token") or has("client_secret")
)] | length == 0
' "$catalog" >/dev/null; then
printf '%s\n' 'schema catalog contains runtime endpoint or credential fields' >&2
exit 1
fi
if policy_search_paths 'mcp-gw\.dingtalk\.com|mcp\.dingtalk\.com/server|Authorization[^[:alnum:]]*:|Bearer [A-Za-z0-9]|access[_-]?token|client[_-]?secret' \
"$catalog" \
internal/cli/schema_parameter_mapping_ledger.go; then
printf '%s\n' 'schema assets contain endpoint or credential material' >&2
exit 1
fi
if [ -e internal/cli/schema_agent_metadata ] || [ -e internal/cli/schema_agent_metadata_audit.json ]; then
printf '%s\n' 'retired schema_agent_metadata delivery artifact must not be present' >&2
exit 1
fi
if policy_search_go '\.ListTools\(' internal/app internal/cli; then
printf '%s\n' 'startup/schema packages must not call MCP tools/list' >&2
exit 1
fi
# Single-track delivery (声明即 Catalog): go:generate only refreshes
# param_aliases. Catalog assembly is runtime ResolveSchemaBuild; CI proves
# determinism via check-schema-assembly.sh. Reject committed Catalog generate.
if ! grep -Eq '^//go:generate .*cmd_param_aliases' internal/cli/gen.go; then
printf '%s\n' 'go generate must register the param_aliases generator' >&2
exit 1
fi
if grep -E '^//go:generate' internal/cli/gen.go | grep -Eq 'cmd_schema_catalog'; then
printf '%s\n' 'go generate must not register committed Catalog delivery (cmd_schema_catalog)' >&2
exit 1
fi
if ! grep -Eq 'check-schema-assembly\.sh' Makefile; then
printf '%s\n' 'Makefile must invoke check-schema-assembly.sh for assembly determinism' >&2
exit 1
fi
if grep -E '^//go:generate' internal/cli/gen.go | grep -Eq 'cmd_schema_agent_metadata|schema_agent_metadata'; then
printf '%s\n' 'go generate must not regenerate retired schema_agent_metadata/' >&2
exit 1
fi
# Embedded MCP/parameter metadata is intentionally expensive and must be
# parsed only through its sync.Once accessor. Each raw loader is allowed at
# exactly two production locations: its declaration and the assignment inside
# that accessor. Any third reference is an eager initializer or an accessor
# bypass and fails this static check.
# Agent metadata JSON embed/loader is retired; production must not reopen it.
if policy_search_production_go 'go:embed schema_agent_metadata' internal/cli; then
printf '%s\n' 'schema_agent_metadata must not be re-embedded' >&2
exit 1
fi
if policy_search_production_go 'loadEmbeddedAgentMetadata\(' internal/cli; then
printf '%s\n' 'retired loadEmbeddedAgentMetadata must not remain in production code' >&2
exit 1
fi
if policy_search_production_go 'loadAgentMetadataFixtureFrom\(' internal/cli; then
printf '%s\n' 'Agent metadata fixture loader must stay test-only' >&2
exit 1
fi
# The lazy loaders for the retired MCP pin (loadPinnedMCPMetadata) and the
# 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)\(' \
internal/cli; then
printf '%s\n' 'Schema metadata accessors must not be called from package-scope variable initializers' >&2
exit 1
fi
# Root construction may register the schema command, but app production code
# must never parse or inspect generation metadata. The schema command reads the
# already embedded Catalog only when it is actually executed.
if policy_search_production_go '(loadEmbeddedAgentMetadata|loadPinnedMCPMetadata|loadSchemaParameterBindingSnapshot|runtimeAgentMetadata|runtimeMCPMetadata|runtimeSchemaParameterBindingData|EmbeddedSchemaParameterBindings)\(' \
internal/app; then
printf '%s\n' 'root/app production code must not access Schema generation metadata loaders or accessors' >&2
exit 1
fi
# Gated confirmation truth: live Contract SafetySpec ↔ ToolSpec ↔ runtime gate
# (not Catalog-vs-Catalog provenance). Invokes
# TestUserRequiredSafetyHomologyWithRuntimeGate; also listed in the focused
# ./internal/cli -run whitelist below so a direct schema-catalog run stays
# complete if the helper script is skipped.
./scripts/policy/check-runtime-confirmation-truth.sh
# Run the typed content gates as policy, rather than treating non-empty
# correction/exclusion maps as proof that their exact keys and winners are
# valid against the shipped Catalog (declaration + Cobra; no MCP pin).
# Homology (HOM-*) CI entrypoints: Safety confirmation truth + vocabulary /
# whitelist pins live in ./internal/cli/homology; parameter/help set equality
# is in ./internal/app below; bindings/mapping subset remains in ./internal/cli.
# Keep TestHomologyCIEntrypointsPinned in sync when adding gate IDs to policy.
go test ./internal/cli \
-run '^(TestDeliverySchemaCatalog.*|TestDeliverySchemaAllPayload.*|TestRuntimeSchemaAllPayload.*|TestSchemaAllReturnsCompleteDeliveryLeafSchemas|TestSchemaCatalogDeliveryCompleteness.*|TestValidateSchemaDeliveryInvariants.*|TestSchemaAliasViewProblem.*|TestSchemaDeliveryToolsByCanonical.*|TestSchemaUsesDeliveryCatalogWithoutRuntimeLoad|TestWalkLeafCommandsTraversesAnnotatedHiddenSubtree|TestSchemaParameterBindingsMatchReviewedBaselineAndDeliveryCatalog|TestBindEffectiveCommandRegistryFailsClosedOnInvalidParameterBindingSource|TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys|TestDeliveryCatalogMCPParameterMappingsAreComplete|TestSchemaParameterMappingAuditExclusionRules|TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty|TestRuntimeCommandParameterSpecsPreserveReviewedEmptyPropertyProvenance|TestSchemaParameterBindingActiveBindingsRemainEmpty|TestResolveMetaFailsClosedOnUnusableMetaIndex|TestRuntimeSchemaMetadataLoadsOnlyOnDemand|TestSchemaParameterBindingsPhase2.*)$' \
-count=1
go test ./internal/cli/homology \
-run '^(TestUserRequiredSafetyHomologyWithRuntimeGate|TestHomologyDecisionDocPinsPathAAndGateIDs|TestMCPPassthroughAdmissionExcludesLeafAndShortcut|TestHomologyCIEntrypointsPinned)$' \
-count=1
go test ./internal/helpers \
-run '^(TestSheetConfirmationGuardCoversEveryProtectedLeaf|TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool)$' \
-count=1
go test ./internal/app \
-run '^(TestDeliverySchemaContractMapsToExecutableTree|TestFinalSchemaParametersMatchExecutableHelpFlags|TestDeliverySchemaParametersMatchExecutableHelpFlags|TestSchemaHelpFlagCompletenessRejects.*|TestRuntimeSchemaCompletenessCoversPublicCommandTree|TestReviewedRoutedInterfacesReachFinalSchema|TestViewGetWrappersUsePinnedGetViewsInterface|TestReviewedInterfaceDispositionSourceOwnsRuntimeSurface|TestSheetFinalSchemaConfirmationMatchesRuntimeGuards|TestRegisterPluginHTTPServerDoesNotProbeEndpoint|TestRegisterStdioServerFromManifestDoesNotStartProcess|TestOrdinaryRootCommandsDoNotLoadSchemaMetadata)$' \
-count=1
printf 'schema catalog check: ok (%s products, %s tools)\n' "$catalog_product_count" "$registry_count"