Files
50292cc754 fix: centralize command validation in corecmd (#1292)
* fix: normalize command validation errors

* fix: normalize command validation errors

* fix: preserve non-validation pre-run errors

* fix: preserve non-validation pre-run errors

* fix: centralize command validation lifecycle

* fix: preserve validation across Cobra traversal and proxies

* chore: preserve upstream Cobra source formatting

* ci: bind Cobra compatibility checks to the PR head

* docs: record completed validation architecture review

* fix: preserve validation failure ownership and lock regression boundaries

* fix: type native Cobra validation for generated commands

* fix: classify legacy Cobra argument lookup failures

* test(app): close the file logger before Windows temp dir cleanup

Executing the runtime root opens <config dir>/logs/dws.log and keeps that
handle for the process lifetime. With DWS_CONFIG_DIR pointed at a
t.TempDir(), Windows cannot remove the directory while the handle is open,
so TestCrossPlatformCoverageTypedValidationErrorGateExtensions failed only
on the Windows coverage job while passing everywhere else.

Register CloseFileLogger after the TempDir so LIFO cleanup releases the
handle first, matching the convention the credential and skill tests use.

* test: cover the fail-closed branches the platform coverage gate counts

The macOS and Windows gates require 100% coverage of changed statements
but only execute TestCrossPlatformCoverage*/TestAllShortcuts* tests, so
three changed statements stayed uncovered even though the full suite passed:

- ResultInvoke rejected without an active unified-result rollout was already
  tested, only under a name the gate filter skips. Rename it.
- ExecuteCForTest propagating a PrepareCommandTree failure, reachable when
  the root itself is unprepared but a descendant already is.
- Root assembly panicking instead of returning a half-adapted tree when a
  mount has already been prepared.

* test(corecmd): cover the ExecuteContext*ForTest success path in package

The aggregate coverage gate assembles per-shard profiles whose -coverpkg is
derived from each shard's changed packages, so a statement in internal/corecmd
exercised only by internal/app or internal/helpers callers can stay uncovered
in the union even though the platform gate, which instruments all four packages
at once, reports 100%. ExecuteContextCForTest's SetContext-and-delegate path
was in that position: in-package tests only reached its nil guard.

Cover it from inside the package so the statement no longer depends on
cross-package instrumentation.

* test(helpers): run standalone whiteboard tests through the prepared tree

Merging main brought in nine new whiteboard tests that execute through bare
cmd.Execute(). A standalone Cobra execution never installs the framework's
validation adapters, so those tests exercised the unprepared path while the
nine pre-existing tests in the same file already used corecmd.ExecuteForTest.

Route all of them through ExecuteForTest so the whole file asserts against the
prepared tree, matching the contract that standalone command tests use the
corecmd *ForTest helpers. Every migrated site used only the returned error, so
the change is one-to-one.

* docs: require standalone command tests to run through the prepared tree

AGENTS.md said standalone command tests may use the corecmd *ForTest helpers.
Permissive wording let a merge from main bring in nine whiteboard tests that
execute through bare cmd.Execute(), which never installs the preparation-stage
validation adapters, so they asserted against the un-adapted path and a
parameter-validation regression would have passed silently.

Make the helper mandatory, record why bare execution is unsafe, and state that
tests arriving from main are in scope so a merge has to re-check them.

* docs: scope the ForTest requirement to test-constructed commands

The rule as first written also flagged root.Execute() after NewRootCommand(),
which is correct code: the app factory already prepared that tree, so bare
Execute runs the adapted path. An over-broad rule forces pointless churn and
cries wolf on valid tests, so exempt factory roots and name the real risk,
which is a tree the test built itself and never prepared.

* fix(errors): keep deadline classification at the business error boundary

WrapErrorWithOperation switched its pass-through guard from "is a structured
*apperrors.Error" to PreserveClassification. That predicate also returns true
for the cancellation and deadline sentinels, so a real context.DeadlineExceeded
left the wrapper untouched, never reached the network-timeout branch, and fell
through to apperrors.ExitCode as internal/exit 5 — losing NETWORK_TIMEOUT, the
API exit code and the retry hint. resolveFileDomain now preserves and wraps
deadlines explicitly, so genuine request timeouts hit this reliably.

Split the concept instead of reverting it, because both behaviours are correct
in their own place. DeclaresClassification recognises only errors carrying a
contract of their own, a structured *Error or an ExitCoder, and is what a
classification boundary should use. PreserveClassification keeps adding
cancellation and deadline identity for validation boundaries, where a timeout
must never be rewritten as a parameter failure.

The existing message table did not catch this: it feeds errors.New(text), and
errors.Is matches only the real sentinel. The regression was in fact pinned by a
test asserting that a wrapped deadline passes through unchanged, so that
assertion is corrected and the sentinel is now tested bare and wrapped, with a
negative control confirming it fails against the old predicate.

* docs(pr): add drive/errors/leaf/oa/recruit/wiki CI evidence

Add a local command-CI collage for PR #1292 covering the Auto CR
required domains, generated from passing focused tests on 7cb5c4dc.

Co-authored-by: john <typefield@users.noreply.github.com>

* test(helpers): compare whiteboard export path via JSON on Windows

Coverage (Windows) failed TestCrossPlatformCoverageWhiteboardExportDownloadsUsingBoardName
because JSON encodes backslashes, so a raw filepath substring no longer matches.

Co-authored-by: john <typefield@users.noreply.github.com>

* Sync merge tree rename deletions

* fix(test): keep result-store installation store-only; tidy go.sum

EmitStoredResult must stay with the caller: unified-result tests set the
output writer after execution and emit themselves, so an automatic emit
inside ExecuteCForTest wrote to the default writer and consumed the
store's emitAttempted, leaving captured stdout empty. go mod tidy drops
cobra v1.10.2 checksums orphaned by the merge (Policy tidy gate).

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: john <typefield@users.noreply.github.com>
2026-09-17 17:17:01 +08:00

295 lines
14 KiB
Makefile

GO ?= go
DWS_PACKAGE_VERSION ?= 0.0.0-test
REMOTE ?=
PUBLISH ?= 0
YES ?= 0
DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp
POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
SCHEMA_CATALOG_OUTPUT ?= artifacts/schema_catalog
SCHEMA_META_INDEX_OUTPUT ?= artifacts/schema_meta_index.gob
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
.PHONY: all help build check-safechat test-aem test-safechat rebuild test test-plan test-auth-legacy-compat typed-validation-errors shortcut-public-e2e-proof lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity skill-context-budget multi-im-skill-chain-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema check-schema-cache-proto fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
all: setup-hooks fmt lint build test rebuild
help:
@printf "Available targets:\n"
@printf " make build - Build the dws CLI binary\n"
@printf " make test - Run the Go test suite\n"
@printf " make check-safechat - Compile and vet the SafeChat message-crypto backend (needs CGO)\n"
@printf " make test-safechat - Run the message-crypto tests against the SafeChat backend\n"
@printf " make test-plan - Verify CI test and full-suite coverage package plans cover their scopes exactly once\n"
@printf " make test-auth-legacy-compat - Run stable legacy authentication compatibility regressions\n"
@printf " make typed-validation-errors - Enforce typed framework parameter-validation boundaries\n"
@printf " make shortcut-public-e2e-proof - Prove every reviewed Devdoc/HRbrain/PAT public Shortcut through exact and owning raw execution\n"
@printf " make lint - Run formatting checks, go vet, and staticcheck\n"
@printf " make format-check - Check all repository Go source files with gofmt\n"
@printf " make fmt - Format all repository Go source files\n"
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
@printf " make interface-integrity [BASE_REF=<ref>] [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check authoritative CLI history\n"
@printf " make authoritative-interface-integrity BASE_REF=<ref> [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check Git-owned CLI history\n"
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and 100%% changed-code coverage\n"
@printf " make coverage-gate-platform BASE_REF=<ref> PROFILE=<file> - Enforce 100%% native changed-code coverage\n"
@printf " make update-interface-baseline - Update the non-authoritative CLI smoke fixture\n"
@printf " make reset-interface-baseline - DANGEROUS: replace the non-authoritative CLI smoke fixture\n"
@printf " make schema-compatibility BASE_REF=<ref> [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check the authoritative Schema history\n"
@printf " make skill-command-integrity - Check dws commands referenced by skills exist\n"
@printf " make skill-context-budget - Check generated Skill drift and common-path context budgets\n"
@printf " make multi-im-skill-chain-integrity - Check reviewed IM intents keep one default Skill route\n"
@printf " make cli-smoke - Verify help for every public top-level command\n"
@printf " make mock-mcp-smoke - Verify HTTP and stdio MCP request/response transport\n"
@printf " make test-schema-agent-examples - Contract-check all Agent examples and dry-run the eligible subset\n"
@printf " make generate-schema - Refresh param_aliases + verify Schema assembly determinism\n"
@printf " make generate-schema-catalog - Optional assembled Catalog dump under artifacts/ (not a delivery step)\n"
@printf " make package - Build all release artifacts locally\n"
@printf " make changelog-pre VERSION=vX.Y.Z-beta.N - Prepare prerelease notes\n"
@printf " make changelog-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N - Prepare stable notes\n"
@printf " make release-pre VERSION=vX.Y.Z-beta.N - Validate prerelease; publish official releases from Actions\n"
@printf " make release-stable VERSION=vX.Y.Z FROM_BETA=vX.Y.Z-beta.N - Validate stable; publish official releases from Actions\n"
@printf " make publish-homebrew-formula - Push dist/homebrew/dingtalk-workspace-cli.rb to a tap repo\n"
build:
@./scripts/dev/build.sh
rebuild:
@./scripts/dev/build.sh
check-safechat:
@CGO_ENABLED=1 $(GO) build ./cmd ./internal/msgcrypto/...
@CGO_ENABLED=1 $(GO) vet ./internal/msgcrypto/...
test-aem:
@mkdir -p "$(POLICY_GOTMPDIR)"
@$(POLICY_ENV) $(GO) -C third_party/aem-go-sdk test -count=1 -timeout=2m ./...
test-safechat:
@CGO_ENABLED=1 $(GO) test -count=1 ./internal/msgcrypto/...
test:
@DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" $(GO) test -count=1 -timeout=10m ./...
test-plan:
@./scripts/ci/test-packages.sh verify
test-auth-legacy-compat:
@mkdir -p "$(POLICY_GOTMPDIR)"
@GO="$(GO)" $(POLICY_ENV) ./scripts/policy/check-auth-legacy-compat.sh
typed-validation-errors:
@GO="$(GO)" ./scripts/policy/check-typed-validation-errors.sh
shortcut-public-e2e-proof: build
@GO="$(GO)" DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" ./scripts/policy/check-shortcut-public-e2e-proof.sh
lint:
@./scripts/dev/lint.sh
format-check:
@set -eu; \
go_files="$$(mktemp "$${TMPDIR:-/tmp}/dws-go-files.XXXXXX")"; \
trap 'rm -f "$$go_files"' EXIT HUP INT TERM; \
$(GO_SOURCE_LIST) > "$$go_files"; \
unformatted="$$(xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -l -- "$$@"; fi' sh < "$$go_files")"; \
if [ -n "$$unformatted" ]; then \
printf '%s\n' "$$unformatted"; \
printf '%s\n' "Go files are not formatted. Run 'make fmt'." >&2; \
exit 1; \
fi
fmt:
@set -eu; \
go_files="$$(mktemp "$${TMPDIR:-/tmp}/dws-go-files.XXXXXX")"; \
trap 'rm -f "$$go_files"' EXIT HUP INT TERM; \
$(GO_SOURCE_LIST) > "$$go_files"; \
xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -w -- "$$@"; fi' sh < "$$go_files"
policy: test-aem test-auth-legacy-compat typed-validation-errors shortcut-public-e2e-proof
@mkdir -p "$(POLICY_GOTMPDIR)"
@$(POLICY_ENV) ./scripts/policy/check-runtime-payload.sh --allow-unsupported-tools
@$(POLICY_ENV) ./scripts/build/generate-runtime-payload-assets.sh --check
@$(POLICY_ENV) ./scripts/policy/check-open-source-assets.sh
@$(POLICY_ENV) ./scripts/policy/check-skill-context-budget.sh
@$(POLICY_ENV) ./scripts/policy/check-multi-im-skill-chain.sh
@$(POLICY_ENV) ./scripts/policy/check-multi-doc-skill-chain.sh
@python3 scripts/run_chat_shortcut_live_audit_test.py
@$(POLICY_ENV) ./scripts/policy/check-command-surface.sh --strict
@SCHEMA_CACHE_PROTO_CHECK=1 $(POLICY_ENV) ./scripts/policy/check-generated-drift.sh
@$(POLICY_ENV) ./scripts/policy/check-module-tidy.sh
@$(POLICY_ENV) ./scripts/policy/check-param-concepts.sh
@$(POLICY_ENV) ./scripts/policy/check-param-alias-cooccurrence.sh
@$(POLICY_ENV) $(GO) test -count=1 ./internal/app -run '^(TestParamAlias(FixtureThroughEmbeddedDeliveryPath|ReadCommandFinalPayload|WriteCommandFinalPayload|CanonicalConflictFailsBeforeRunE|BlockedFlagReachesReviewedFinalError)|TestFlagConflictErrorFormattingIsDeterministic)$$'
@$(POLICY_ENV) ./scripts/policy/check-schema-catalog.sh
@$(POLICY_ENV) ./scripts/policy/check-schema-binary.sh
@$(POLICY_ENV) $(MAKE) test-schema-agent-examples
edition-test:
$(GO) test -v -count=1 ./pkg/editiontest/...
interface-integrity:
@base_ref="$(BASE_REF)"; \
candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$base_ref" ]; then base_ref="origin/main"; fi; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-interface-baselines.sh \
--base-ref "$$base_ref" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
authoritative-interface-integrity:
@candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-interface-baselines.sh \
--base-ref "$(BASE_REF)" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
coverage-gate:
@./scripts/policy/check-coverage-gate.sh --base-ref "$(BASE_REF)" --scope-buildable
coverage-gate-platform:
@./scripts/policy/run-platform-coverage-gate.sh --base-ref "$(BASE_REF)" --profile "$(PROFILE)"
update-interface-baseline:
@./scripts/policy/check-interface-baseline.sh --update
reset-interface-baseline:
@./scripts/policy/check-interface-baseline.sh --reset
schema-compatibility:
@candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-schema-compatibility.sh \
--base-ref "$(BASE_REF)" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
skill-command-integrity:
@./scripts/policy/check-skill-commands.sh
skill-context-budget:
@./scripts/policy/check-skill-context-budget.sh
multi-im-skill-chain-integrity:
@./scripts/policy/check-multi-im-skill-chain.sh
multi-doc-skill-chain-integrity:
@./scripts/policy/check-multi-doc-skill-chain.sh
skill-mono-multi-content:
@./scripts/policy/check-mono-multi-skill-content.sh
cli-smoke:
@./scripts/policy/check-cli-smoke.sh
mock-mcp-smoke:
$(GO) test -v -count=1 -run '^(TestHTTPClientEndToEnd|TestStdioClientEndToEnd)$$' ./internal/transport
test-schema-agent-examples:
DWS_AGENT_EXAMPLES_DRY_RUN=1 $(GO) test -v -count=1 ./internal/app -run '^TestAgentExamplesDryRun$$'
# generate-schema refreshes param_aliases_generated.go and verifies that
# ResolveSchemaBuild assembly is deterministic. Catalog is runtime-assembled
# (声明即 Catalog); cmd_schema_catalog is not a committed delivery step.
# schema_agent_metadata/ and schema_hints/ must stay absent.
generate-schema:
@set -e; \
concepts_guard=$$(mktemp); \
concepts_schema_guard=$$(mktemp); \
command_fallbacks_guard=$$(mktemp); \
command_fallbacks_schema_guard=$$(mktemp); \
trap 'rm -f "$$concepts_guard" "$$concepts_schema_guard" "$$command_fallbacks_guard" "$$command_fallbacks_schema_guard"' EXIT HUP INT TERM; \
cp internal/cli/param_concepts.json "$$concepts_guard"; \
cp internal/cli/param_concepts.schema.json "$$concepts_schema_guard"; \
cp internal/cli/command_path_fallbacks.json "$$command_fallbacks_guard"; \
cp internal/cli/command_path_fallbacks.schema.json "$$command_fallbacks_schema_guard"; \
$(GO) generate ./internal/cli; \
rm -rf internal/cli/schema_agent_metadata internal/cli/schema_agent_metadata_audit.json; \
rm -f internal/cli/schema_meta_index.json; \
if [ -e internal/cli/schema_command_registry ]; then \
printf '%s\n' 'retired schema_command_registry/ must not reappear after generation' >&2; \
exit 1; \
fi; \
cmp -s internal/cli/param_concepts.json "$$concepts_guard" || { \
printf '%s\n' 'generation modified reviewed input internal/cli/param_concepts.json' >&2; \
exit 1; \
}; \
cmp -s internal/cli/param_concepts.schema.json "$$concepts_schema_guard" || { \
printf '%s\n' 'generation modified reviewed input internal/cli/param_concepts.schema.json' >&2; \
exit 1; \
}; \
cmp -s internal/cli/command_path_fallbacks.json "$$command_fallbacks_guard" || { \
printf '%s\n' 'generation modified reviewed input internal/cli/command_path_fallbacks.json' >&2; \
exit 1; \
}; \
cmp -s internal/cli/command_path_fallbacks.schema.json "$$command_fallbacks_schema_guard" || { \
printf '%s\n' 'generation modified reviewed input internal/cli/command_path_fallbacks.schema.json' >&2; \
exit 1; \
}; \
if [ -e internal/cli/schema_hints ]; then \
printf '%s\n' 'retired schema_hints/ must not reappear after generation' >&2; \
exit 1; \
fi; \
if [ -e internal/cli/schema_meta_index.json ]; then \
printf '%s\n' 'retired schema_meta_index.json must not remain after generation' >&2; \
exit 1; \
fi; \
./scripts/policy/check-schema-assembly.sh
# Optional local/CI dump of an assembled Catalog under artifacts/ by default.
# Override SCHEMA_CATALOG_OUTPUT and SCHEMA_META_INDEX_OUTPUT as needed. This
# is not a go:generate or production delivery step.
check-schema-cache-proto:
@SCHEMA_CACHE_PROTO_CHECK=1 ./scripts/generate-schema-cache-proto.sh --check
generate-schema-catalog:
$(GO) run -a ./internal/generator/cmd_schema_catalog \
-root . \
-output "$(SCHEMA_CATALOG_OUTPUT)" \
-meta-index "$(SCHEMA_META_INDEX_OUTPUT)"
fetch-mcp-metadata:
@printf ' %sFetching diagnostic MCP dump (not a Schema pin)%s\n' "$(COLOR_RUN)" "$(COLOR_RESET)"
@./scripts/dev/fetch_mcp_metadata.sh
package:
@version="$(if $(VERSION),$(VERSION),v0.0.0-SNAPSHOT)"; VERSION="$${version#v}" ./scripts/dev/build-all.sh
@version="$(if $(VERSION),$(VERSION),v0.0.0-SNAPSHOT)"; DWS_PACKAGE_VERSION="$$version" ./scripts/release/post-goreleaser.sh
publish-homebrew-formula:
@./scripts/release/publish-homebrew-formula.sh
setup-hooks:
@git config core.hooksPath scripts/hooks 2>/dev/null || true
changelog-pre:
@test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3-beta.1\n' >&2; exit 2)
@./scripts/release/prepare-changelog.sh prerelease "$(VERSION)"
changelog-stable:
@test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3\n' >&2; exit 2)
@test -n "$(FROM_BETA)" || (printf 'FROM_BETA is required, e.g. v1.2.3-beta.2\n' >&2; exit 2)
@./scripts/release/prepare-changelog.sh stable "$(VERSION)" --from-beta "$(FROM_BETA)"
release-pre:
@test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3-beta.1\n' >&2; exit 2)
@test -n "$(REMOTE)" || (printf 'REMOTE is required, e.g. origin\n' >&2; exit 2)
@args=""; \
if [ "$(PUBLISH)" = "1" ]; then args="$$args --publish"; fi; \
if [ "$(YES)" = "1" ]; then args="$$args --yes"; fi; \
./scripts/release/release.sh prerelease "$(VERSION)" --remote "$(REMOTE)" $$args
release-stable:
@test -n "$(VERSION)" || (printf 'VERSION is required, e.g. v1.2.3\n' >&2; exit 2)
@test -n "$(FROM_BETA)" || (printf 'FROM_BETA is required, e.g. v1.2.3-beta.2\n' >&2; exit 2)
@test -n "$(REMOTE)" || (printf 'REMOTE is required, e.g. origin\n' >&2; exit 2)
@args=""; \
if [ "$(PUBLISH)" = "1" ]; then args="$$args --publish"; fi; \
if [ "$(YES)" = "1" ]; then args="$$args --yes"; fi; \
./scripts/release/release.sh stable "$(VERSION)" --from-beta "$(FROM_BETA)" --remote "$(REMOTE)" $$args
release:
@printf 'Use make release-pre or make release-stable; direct goreleaser publishing is disabled.\n' >&2
@exit 2