Compare commits

...
Author SHA1 Message Date
修雨 7fddace8df ci: enforce complete Go test coverage 2026-07-21 09:41:18 +08:00
修雨 076d77da8e Merge pull request #699 from DingTalk-Real-AI/codex/ci-coverage-100
ci: shorten workflow name and require 100% changed-code coverage
2026-07-20 17:44:56 +08:00
修雨 6e070a7e24 ci: tighten PR coverage gate 2026-07-20 17:14:35 +08:00
27 changed files with 676 additions and 376 deletions
+1
View File
@@ -95,6 +95,7 @@ jobs:
typeof filename === 'string' &&
(
filename.startsWith('.github/workflows/') ||
filename.startsWith('scripts/ci/') ||
filename.startsWith('scripts/policy/') ||
filename.startsWith('scripts/release/') ||
filename === 'test/fixtures/cli-interface-baseline.txt' ||
+26 -32
View File
@@ -1,4 +1,4 @@
name: Code Admission — PR 合入门禁
name: CI
on:
push:
@@ -130,11 +130,13 @@ jobs:
with:
go-version-file: go.mod
- name: Verify Test Package Plan
if: steps.classify.outputs.changelog_only != 'true'
run: make test-plan
- name: Format Check
if: steps.classify.outputs.changelog_only != 'true'
run: |
unformatted="$(find cmd internal test scripts/policy -name '*.go' -print0 | xargs -0r gofmt -l)"
test -z "$unformatted" || (printf '%s\n' "$unformatted" && exit 1)
run: make format-check
- name: Go Vet
if: steps.classify.outputs.changelog_only != 'true'
@@ -167,38 +169,20 @@ jobs:
with:
go-version-file: go.mod
- name: Install archive tooling
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Build
if: ${{ matrix.shard == 'remaining' }}
run: make build
- name: Test shard with Race Detection
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
TEST_SHARD: ${{ matrix.shard }}
run: |
case "$TEST_SHARD" in
app)
packages=(./internal/app/...)
;;
generators)
packages=(./internal/generator/...)
;;
helpers)
packages=(./internal/helpers/...)
;;
remaining)
mapfile -t packages < <(
go list ./cmd/... ./internal/... |
grep -Ev '/internal/(app|generator|helpers)(/|$)'
)
;;
*)
printf 'unknown test shard: %s\n' "$TEST_SHARD" >&2
exit 1
;;
esac
set -euo pipefail
package_output="$(./scripts/ci/test-packages.sh list "$TEST_SHARD")"
test -n "$package_output"
mapfile -t packages <<< "$package_output"
test "${#packages[@]}" -gt 0
go test -v -race -count=1 -timeout=10m "${packages[@]}"
@@ -207,7 +191,7 @@ jobs:
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -221,7 +205,16 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Test release scripts
run: go test -v -count=1 -timeout=5m ./test/scripts
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
run: |
set -euo pipefail
package_output="$(./scripts/ci/test-packages.sh list release-scripts)"
test -n "$package_output"
mapfile -t packages <<< "$package_output"
test "${#packages[@]}" -gt 0
go test -v -count=1 -timeout=10m "${packages[@]}"
test-cross-platform:
name: Test (cross-platform compile)
@@ -714,9 +707,10 @@ jobs:
- name: Enforce coverage gate
if: needs.lint.outputs.changelog_only != 'true'
env:
COVERAGE_TARGET: "80"
COVERAGE_TARGET: "100"
COVERAGE_ENFORCE_OVERALL: "false"
run: COVERAGE_ADDITIONAL_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
COVERAGE_OVERALL_TOLERANCE: "0"
run: COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
- name: Generate coverage report
if: needs.lint.outputs.changelog_only != 'true'
+1 -1
View File
@@ -3,7 +3,7 @@ name: Main Integration — Wukong Overlay
on:
workflow_run:
workflows:
- Code Admission — PR 合入门禁
- CI
types:
- completed
+1 -1
View File
@@ -36,8 +36,8 @@ Common repository checks already used here include:
./scripts/policy/check-open-source-assets.sh
go test ./...
make test
make test-plan
make lint
bash test/scripts/run_all_tests.sh --jobs 8
./scripts/policy/check-generated-drift.sh
./scripts/policy/check-command-surface.sh --strict
./scripts/release/verify-package-managers.sh
+30 -7
View File
@@ -1,12 +1,14 @@
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
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 rebuild test lint 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 cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
.PHONY: all help build rebuild test test-plan 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 cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-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
@@ -14,13 +16,15 @@ 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 lint - Run formatting checks and golangci-lint when available\n"
@printf " make fmt - Format Go source files\n"
@printf " make test-plan - Verify every default Go package belongs to one CI test shard\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 - Check historical commands and help contracts still work\n"
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and changed-code coverage\n"
@printf " make coverage-gate-platform BASE_REF=<ref> PROFILE=<file> - Enforce native-platform changed-code coverage\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 - Add new CLI contracts without removing history\n"
@printf " make reset-interface-baseline - DANGEROUS: replace all CLI compatibility history\n"
@printf " make schema-compatibility BASE_REF=<ref> - Check the complete Schema contract against the PR merge-base\n"
@@ -45,13 +49,32 @@ rebuild:
@./scripts/dev/build.sh
test:
@./test/scripts/run_all_tests.sh --timeout 5m
@DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" $(GO) test -count=1 -timeout=10m ./...
test-plan:
@./scripts/ci/test-packages.sh verify
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:
@find cmd internal test scripts/policy -name '*.go' -print0 2>/dev/null | xargs -0r gofmt -w
@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:
@mkdir -p "$(POLICY_GOTMPDIR)"
+1 -1
View File
@@ -52,7 +52,7 @@ run.
```mermaid
flowchart TB
PR["Pull request"] --> CA["Code Admission — PR 合入门禁"]
PR["Pull request"] --> CA["CI"]
subgraph CA_CHECKS["Nine required contexts"]
L["Lint"]
T["Test"]
+11 -5
View File
@@ -1,4 +1,4 @@
# Code Admission — PR 合入门禁
# CI — PR 合入门禁
The pull-request admission layer has exactly nine required external contexts:
@@ -6,7 +6,7 @@ The pull-request admission layer has exactly nine required external contexts:
|---|---|
| `Lint` | Stable PR revision classification, formatting, `go vet`, and Actionlint |
| `Test` | Race/unit/release-script tests plus fast cross-platform compilation |
| `Coverage` | Overall non-regression and changed-code coverage |
| `Coverage` | Overall non-regression and 100% changed-code coverage |
| `Policy` | Repository policy and the fail-closed CHANGELOG contract |
| `Edition` | Edition contract tests |
| `Interface Integrity` | CLI, Schema, Skill, and stable-release compatibility |
@@ -14,7 +14,7 @@ The pull-request admission layer has exactly nine required external contexts:
| `CLI Smoke` | Offline help for every public top-level command |
| `Mock MCP` | HTTP and stdio MCP lifecycle smoke tests |
The workflow display name is `Code Admission — PR 合入门禁`. Parallel helper
The workflow display name is `CI`. Parallel helper
jobs may implement `Test` and `Coverage`, but they are not ruleset contexts.
Do not require an aggregate alias or a downstream integration check in place of
the nine contracts above.
@@ -81,7 +81,7 @@ successful PR check.
```mermaid
flowchart TB
PR["Pull request"] --> ADMISSION["Code Admission — PR 合入门禁"]
PR["Pull request"] --> ADMISSION["CI"]
ADMISSION --> L["Lint"]
ADMISSION --> T["Test"]
ADMISSION --> C["Coverage"]
@@ -122,7 +122,13 @@ base_ref=$(git merge-base HEAD origin/main)
`make coverage-gate` is an enforcement step, not a profile generator. CI
generates the candidate, supporting, merge-base, and (when risk-selected)
native profiles before the aggregate `Coverage` context evaluates them.
native profiles before the aggregate `Coverage` context evaluates them. The
aggregate and native gates require 100% coverage for changed executable Go
statements. Overall coverage remains an unrounded, zero-tolerance merge-base
non-regression check. Candidate and baseline profiles are evaluated by the
same block-deduplicating checker; supporting policy and shortcut profiles
contribute to changed-code coverage only. The checked-in badge is presentation
only and is never read as a gate input.
Compatibility checks derive authoritative Interface snapshots from the PR
merge-base and the latest reachable stable release. The candidate cannot bless
+1 -1
View File
@@ -2,7 +2,7 @@
发布只走一条链路:本地脚本负责封板、验证并推送 annotated tag;GitHub Actions 负责构建和发布最终产物。不要直接运行 `goreleaser release`,也不要手工补打或移动 tag。
发布前必须完成平台治理:目标 GitHub 仓库已启用 immutable releases,`main` 精确要求 `Code Admission — PR 合入门禁` 的九个 context:`Lint`、`Test`、`Coverage`、`Policy`、`Edition`、`Interface Integrity`、`AI Behavior`、`CLI Smoke`、`Mock MCP`,操作机已安装并登录 `gh`。本地脚本会在封 tag 前通过 API 检查 immutable releases、当前 SHA 的全部九个 context 和在途 Release;`v*` tag ruleset 仍需仓库管理员预先配置并由操作人确认。
发布前必须完成平台治理:目标 GitHub 仓库已启用 immutable releases,`main` 精确要求 `CI` workflow 的九个 context:`Lint`、`Test`、`Coverage`、`Policy`、`Edition`、`Interface Integrity`、`AI Behavior`、`CLI Smoke`、`Mock MCP`,操作机已安装并登录 `gh`。本地脚本会在封 tag 前通过 API 检查 immutable releases、当前 SHA 的全部九个 context 和在途 Release;`v*` tag ruleset 仍需仓库管理员预先配置并由操作人确认。
## 日常只用一个入口
+61 -17
View File
@@ -31,6 +31,31 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
)
type consumeReadyWriter struct {
ready chan struct{}
once sync.Once
}
func newConsumeReadyWriter() *consumeReadyWriter {
return &consumeReadyWriter{ready: make(chan struct{})}
}
func (w *consumeReadyWriter) Write(p []byte) (int, error) {
if bytes.Contains(p, []byte("[event] ready ")) {
w.once.Do(func() { close(w.ready) })
}
return len(p), nil
}
func waitForConsumerReady(t *testing.T, ready <-chan struct{}) {
t.Helper()
select {
case <-ready:
case <-time.After(3 * time.Second):
t.Fatal("consumer did not report ready")
}
}
// TestIntegration_HelloPushdownFiltersAtBus verifies the Hello-time
// event_types pushdown contract (plan §4 unsung superpower): a consumer
// subscribing to "im.*" must NOT receive "approval.*" events even when
@@ -49,40 +74,50 @@ func TestIntegration_HelloPushdownFiltersAtBus(t *testing.T) {
defer func() { cancel(); <-runDone }()
var imBuf, approvalBuf bytes.Buffer
var wg sync.WaitGroup
wg.Add(2)
imReady := newConsumeReadyWriter()
approvalReady := newConsumeReadyWriter()
consumeCtx, cancelConsumers := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelConsumers()
consumeDone := make(chan error, 2)
// Consumer A: im.* only
go func() {
defer wg.Done()
_ = Run(context.Background(), Config{
consumeDone <- Run(consumeCtx, Config{
WorkDir: dir,
IPCEndpoint: sock,
ClientID: "ding_test",
Stdout: &imBuf,
Stderr: io.Discard,
Stderr: imReady,
EventTypes: []string{"im.*"},
MaxEvents: 3, // 3 im events expected
})
}()
// Consumer B: approval.* only
go func() {
defer wg.Done()
_ = Run(context.Background(), Config{
consumeDone <- Run(consumeCtx, Config{
WorkDir: dir,
IPCEndpoint: sock,
ClientID: "ding_test",
Stdout: &approvalBuf,
Stderr: io.Discard,
Stderr: approvalReady,
EventTypes: []string{"approval.*"},
MaxEvents: 2, // 2 approval events expected
})
}()
// Give both consumers time to Hello + register.
time.Sleep(200 * time.Millisecond)
waitForConsumerReady(t, imReady.ready)
waitForConsumerReady(t, approvalReady.ready)
close(trigger)
wg.Wait()
for range 2 {
select {
case err := <-consumeDone:
if err != nil {
t.Fatalf("consume failed: %v", err)
}
case <-consumeCtx.Done():
t.Fatalf("consumers did not finish: %v", consumeCtx.Err())
}
}
// Verify consumer A got exactly the 3 im.* events.
imLines := nonEmptyLines(imBuf.String())
@@ -130,23 +165,32 @@ func TestIntegration_FilterRegexNarrowsFurther(t *testing.T) {
defer func() { cancel(); <-runDone }()
var buf bytes.Buffer
consumeDone := make(chan struct{})
ready := newConsumeReadyWriter()
consumeCtx, cancelConsumer := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelConsumer()
consumeDone := make(chan error, 1)
go func() {
defer close(consumeDone)
_ = Run(context.Background(), Config{
consumeDone <- Run(consumeCtx, Config{
WorkDir: dir,
IPCEndpoint: sock,
ClientID: "ding_test",
Stdout: &buf,
Stderr: io.Discard,
Stderr: ready,
EventTypes: []string{"im.*"},
Filter: `\.at_v1$`, // only at_v1 events
MaxEvents: 1,
})
}()
time.Sleep(150 * time.Millisecond)
waitForConsumerReady(t, ready.ready)
close(trigger)
<-consumeDone
select {
case err := <-consumeDone:
if err != nil {
t.Fatalf("consume failed: %v", err)
}
case <-consumeCtx.Done():
t.Fatalf("consumer did not finish: %v", consumeCtx.Err())
}
lines := nonEmptyLines(buf.String())
if len(lines) != 1 {
+2 -2
View File
@@ -20,8 +20,8 @@
// 2. 渐进式退避(1s/2s/5s/10s/15s/30s/30s...,可自定义)
// 3. 默认 5 分钟超时,超时返回 jobId 让用户用 `... get --job-id <ID>` 续等
// 4. 状态机统一:PROCESSING/PENDING → SUCCESS → 拿 downloadUrl
// ↓
// FAILED → 返回 error
// ↓
// FAILED → 返回 error
// 5. 可选 PUT 下载:传入 OutputPath 时本包负责 HTTP GET 落盘
package asynctask
+3 -1
View File
@@ -112,7 +112,9 @@ func TestSubmit_ContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
submitFn := func(ctx context.Context) (string, error) { return "job-x", nil }
queryFn := func(ctx context.Context, jobID string) (QueryResult, error) { return QueryResult{Status: StatusProcessing}, nil }
queryFn := func(ctx context.Context, jobID string) (QueryResult, error) {
return QueryResult{Status: StatusProcessing}, nil
}
_, err := Submit(ctx, submitFn, queryFn, Options{Backoff: testBackoff, Timeout: 1 * time.Second})
if !errors.Is(err, context.Canceled) {
t.Fatalf("expect ctx.Canceled, got %v", err)
+6 -3
View File
@@ -3,9 +3,11 @@
These repo-local entrypoints are the supported shell entrypoints for building, testing, packaging, and policy checks.
- `make build`: build the `dws` CLI from `cmd`
- `make test`: run `go test ./...`
- `make lint`: run formatting checks and required `golangci-lint`
- `make fmt`: format Go source files under `cmd/`, `internal/`, and `test/`
- `make test`: run every default package returned by `go list ./...`
- `make test-plan`: verify every default Go package belongs to exactly one CI test shard
- `make lint`: run the repository-wide format check, `go vet`, and required `staticcheck`
- `make format-check`: check every tracked or non-ignored untracked repository Go source file with `gofmt`
- `make fmt`: format every tracked or non-ignored untracked repository Go source file
- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first)
- `make package`: build all release artifacts locally
- `make release-pre VERSION=vX.Y.Z-beta.N`: validate a prerelease (`PUBLISH=1` pushes its tag)
@@ -15,6 +17,7 @@ Script groups:
- Root installers: `./scripts/install.sh`, `./scripts/install.ps1`, `./scripts/install-skills.sh`
- Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh`
- CI package plan: `./scripts/ci/test-packages.sh`
- Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh`
- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag>`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace
+121
View File
@@ -0,0 +1,121 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
MODULE="$(cd "$ROOT" && go list -m)"
usage() {
printf '%s\n' \
"usage: $0 list <app|generators|helpers|remaining|release-scripts>" \
" $0 verify" >&2
exit 2
}
list_shard() {
shard="$1"
cd "$ROOT"
case "$shard" in
app)
go list ./internal/app/...
;;
generators)
go list ./internal/generator/...
;;
helpers)
go list ./internal/helpers/...
;;
release-scripts)
go list ./test/scripts/...
;;
remaining)
all_packages="$(go list ./...)"
printf '%s\n' "$all_packages" | while IFS= read -r package; do
case "$package" in
"$MODULE/internal/app"|"$MODULE/internal/app/"*) ;;
"$MODULE/internal/generator"|"$MODULE/internal/generator/"*) ;;
"$MODULE/internal/helpers"|"$MODULE/internal/helpers/"*) ;;
"$MODULE/test/scripts"|"$MODULE/test/scripts/"*) ;;
*) printf '%s\n' "$package" ;;
esac
done
;;
*)
printf 'unknown test package shard: %s\n' "$shard" >&2
exit 2
;;
esac
}
verify_plan() {
workdir="$(mktemp -d "${TMPDIR:-/tmp}/dws-test-packages.XXXXXX")"
trap 'rm -rf "$workdir"' EXIT HUP INT TERM
expected="$workdir/expected"
assigned="$workdir/assigned"
unique="$workdir/unique"
duplicates="$workdir/duplicates"
missing="$workdir/missing"
unexpected="$workdir/unexpected"
all_packages="$workdir/all-packages"
cd "$ROOT"
go list ./... > "$all_packages"
LC_ALL=C sort -u "$all_packages" > "$expected"
: > "$assigned"
for shard in app generators helpers remaining release-scripts; do
shard_packages="$workdir/$shard"
unsorted_shard_packages="$workdir/$shard.unsorted"
list_shard "$shard" > "$unsorted_shard_packages"
LC_ALL=C sort "$unsorted_shard_packages" > "$shard_packages"
if [ ! -s "$shard_packages" ]; then
printf 'test package shard is empty: %s\n' "$shard" >&2
exit 1
fi
cat "$shard_packages" >> "$assigned"
done
LC_ALL=C sort "$assigned" -o "$assigned"
uniq -d "$assigned" > "$duplicates"
LC_ALL=C sort -u "$assigned" > "$unique"
comm -23 "$expected" "$unique" > "$missing"
comm -13 "$expected" "$unique" > "$unexpected"
failed=0
if [ -s "$duplicates" ]; then
printf '%s\n' 'test packages assigned to more than one shard:' >&2
sed 's/^/ /' "$duplicates" >&2
failed=1
fi
if [ -s "$missing" ]; then
printf '%s\n' 'default Go packages missing from the CI test plan:' >&2
sed 's/^/ /' "$missing" >&2
failed=1
fi
if [ -s "$unexpected" ]; then
printf '%s\n' 'CI test plan contains packages outside go list ./...:' >&2
sed 's/^/ /' "$unexpected" >&2
failed=1
fi
if [ "$failed" -ne 0 ]; then
exit 1
fi
package_count="$(wc -l < "$expected" | tr -d ' ')"
printf 'test package plan covers %s default packages exactly once\n' "$package_count"
}
case "${1:-}" in
list)
[ "$#" -eq 2 ] || usage
list_shard "$2"
;;
verify)
[ "$#" -eq 1 ] || usage
verify_plan
;;
*)
usage
;;
esac
+1 -1
View File
@@ -11,7 +11,7 @@ run() {
cd "$ROOT"
run ./scripts/dev/lint.sh
run go test ./...
run make test
COVERAGE_PROFILE="$(mktemp "${TMPDIR:-/tmp}/dws-coverage.XXXXXX")"
run ./scripts/dev/coverage.sh "$COVERAGE_PROFILE"
rm -f "$COVERAGE_PROFILE"
+5 -6
View File
@@ -1,13 +1,12 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
cd "$ROOT"
# ── Format check ──────────────────────────────────────────
unformatted="$(find cmd internal test -name '*.go' -print0 2>/dev/null | xargs -0r gofmt -l)"
if [ -n "$unformatted" ]; then
echo "$unformatted"
echo "Go files are not formatted. Run 'make fmt'." >&2
exit 1
fi
echo "Running gofmt check..."
make format-check
# ── go vet (built-in) ────────────────────────────────────
echo "Running go vet..."
+4 -4
View File
@@ -4,7 +4,7 @@ set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
cd "$ROOT"
go test ./test/mock_mcp/... \
./test/integration/discovery/... \
./test/integration/extensions/... \
./test/integration/recovery/...
DWS_PACKAGE_VERSION="${DWS_PACKAGE_VERSION:-0.0.0-test}" \
go test -count=1 -timeout=5m \
./test/mock_mcp/... \
./test/integration/...
+9 -16
View File
@@ -4,17 +4,17 @@ set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
BASE_REF=""
OVERALL_PROFILE="coverage.txt"
ADDITIONAL_PROFILE="${COVERAGE_ADDITIONAL_PROFILE:-}"
ADDITIONAL_DIFF_PROFILE="${COVERAGE_ADDITIONAL_DIFF_PROFILE:-${COVERAGE_ADDITIONAL_PROFILE:-}}"
BASELINE_PROFILE="coverage-base.txt"
DIFF_PROFILE="coverage-policy.txt"
TARGET="${COVERAGE_TARGET:-80}"
OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0.1}"
TARGET="${COVERAGE_TARGET:-100}"
OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"
ENFORCE_OVERALL="${COVERAGE_ENFORCE_OVERALL:-false}"
CHANGED_ONLY="false"
SCOPE_BUILDABLE="false"
usage() {
printf '%s\n' "usage: $0 --base-ref <ref> [--changed-only] [--scope-buildable] [--overall-profile <file>] [--additional-profile <file>] [--baseline-profile <file>] [--diff-profile <file>]" >&2
printf '%s\n' "usage: $0 --base-ref <ref> [--changed-only] [--scope-buildable] [--overall-profile <file>] [--additional-diff-profile <file>] [--baseline-profile <file>] [--diff-profile <file>]" >&2
}
while [ "$#" -gt 0 ]; do
@@ -29,9 +29,9 @@ while [ "$#" -gt 0 ]; do
OVERALL_PROFILE="$2"
shift 2
;;
--additional-profile)
--additional-diff-profile|--additional-profile)
[ "$#" -ge 2 ] || { usage; exit 2; }
ADDITIONAL_PROFILE="$2"
ADDITIONAL_DIFF_PROFILE="$2"
shift 2
;;
--baseline-profile)
@@ -87,19 +87,12 @@ set -- "$CHECKER" \
if [ "$CHANGED_ONLY" = "true" ]; then
set -- "$@" --changed-only
else
baseline="$(go tool cover -func="$BASELINE_PROFILE" | awk '/^total:/ { gsub(/%/, "", $3); print $3 }')"
[ -n "$baseline" ] || {
printf 'error: cannot parse authoritative coverage from %s\n' "$BASELINE_PROFILE" >&2
exit 2
}
set -- "$@" \
--overall-profile "$OVERALL_PROFILE" \
--diff-profile "$OVERALL_PROFILE" \
--baseline-overall "$baseline"
if [ -n "$ADDITIONAL_PROFILE" ]; then
set -- "$@" \
--overall-profile "$ADDITIONAL_PROFILE" \
--diff-profile "$ADDITIONAL_PROFILE"
--baseline-profile "$BASELINE_PROFILE"
if [ -n "$ADDITIONAL_DIFF_PROFILE" ]; then
set -- "$@" --diff-profile "$ADDITIONAL_DIFF_PROFILE"
fi
fi
if [ "$SCOPE_BUILDABLE" = "true" ]; then
+51 -29
View File
@@ -13,7 +13,6 @@ import (
"flag"
"fmt"
"io"
"math"
"os"
"os/exec"
"path/filepath"
@@ -37,11 +36,13 @@ func (values *stringList) Set(value string) error {
}
type coverageBlock struct {
File string
StartLine int
EndLine int
Statements int
Count int
File string
StartLine int
StartColumn int
EndLine int
EndColumn int
Statements int
Count int
}
type lineRange struct {
@@ -78,6 +79,7 @@ func run(
buildableLoader func() (map[string]bool, error),
) int {
var overallPaths stringList
var baselinePaths stringList
var diffPaths stringList
var baseRef string
var modulePath string
@@ -90,12 +92,13 @@ func run(
flags := flag.NewFlagSet("coverage-gate", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.Var(&overallPaths, "overall-profile", "coverage profile used for overall coverage (repeatable)")
flags.Var(&baselinePaths, "baseline-profile", "merge-base coverage profile evaluated with the same model as the candidate (repeatable)")
flags.Var(&diffPaths, "diff-profile", "coverage profile used for changed-code coverage (repeatable)")
flags.StringVar(&baseRef, "base-ref", "", "Git merge-base or previous main SHA")
flags.StringVar(&modulePath, "module", "", "Go module path used to normalize profile filenames")
flags.Float64Var(&baselineOverall, "baseline-overall", -1, "authoritative overall coverage percentage")
flags.Float64Var(&overallTolerance, "overall-tolerance", 0.1, "allowed overall coverage measurement variance in percentage points")
flags.Float64Var(&target, "target", 80, "required changed-code and eventual overall coverage percentage")
flags.Float64Var(&overallTolerance, "overall-tolerance", 0, "allowed overall coverage measurement variance in percentage points")
flags.Float64Var(&target, "target", 100, "required changed-code coverage percentage and optional overall floor")
flags.BoolVar(&enforceOverall, "enforce-overall-target", false, "require overall coverage to reach target")
flags.BoolVar(&changedOnly, "changed-only", false, "enforce changed-code coverage without an overall baseline")
flags.BoolVar(&scopeBuildable, "scope-buildable", false, "only evaluate changed files buildable on the current platform")
@@ -103,8 +106,13 @@ func run(
return 2
}
if len(diffPaths) == 0 || baseRef == "" || modulePath == "" || (!changedOnly && (len(overallPaths) == 0 || baselineOverall < 0)) {
fmt.Fprintln(stderr, "coverage-gate requires --diff-profile, --base-ref, and --module; overall mode also requires --overall-profile and --baseline-overall")
if len(diffPaths) == 0 || baseRef == "" || modulePath == "" ||
(!changedOnly && (len(overallPaths) == 0 || (len(baselinePaths) == 0 && baselineOverall < 0))) {
fmt.Fprintln(stderr, "coverage-gate requires --diff-profile, --base-ref, and --module; overall mode also requires --overall-profile and either --baseline-profile or --baseline-overall")
return 2
}
if len(baselinePaths) > 0 && baselineOverall >= 0 {
fmt.Fprintln(stderr, "coverage-gate accepts either --baseline-profile or --baseline-overall, not both")
return 2
}
var overall []coverageBlock
@@ -115,6 +123,14 @@ func run(
fmt.Fprintln(stderr, err)
return 2
}
if len(baselinePaths) > 0 {
baseline, baselineErr := readProfiles(baselinePaths, modulePath)
if baselineErr != nil {
fmt.Fprintln(stderr, baselineErr)
return 2
}
baselineOverall = coveragePercent(baseline)
}
}
diff, err := readProfiles(diffPaths, modulePath)
if err != nil {
@@ -150,12 +166,12 @@ func run(
if enforceOverall {
mode = "required"
}
fmt.Fprintf(stdout, "overall coverage: %.1f%% (merge-base %.1f%%; tolerance %.1fpp; target %.1f%%; %s)\n", result.Overall, baselineOverall, overallTolerance, target, mode)
fmt.Fprintf(stdout, "overall coverage: %.4f%% (merge-base %.4f%%; tolerance %.4fpp; target %.4f%%; %s)\n", result.Overall, baselineOverall, overallTolerance, target, mode)
}
if result.ChangedStatements == 0 {
fmt.Fprintf(stdout, "changed code coverage: n/a (no changed executable statements; target %.1f%%)\n", target)
fmt.Fprintf(stdout, "changed code coverage: n/a (no changed executable statements; target %.4f%%)\n", target)
} else {
fmt.Fprintf(stdout, "changed code coverage: %.1f%% (%d executable statements; target %.1f%%)\n", result.ChangedCoverage, result.ChangedStatements, target)
fmt.Fprintf(stdout, "changed code coverage: %.4f%% (%d executable statements; target %.4f%%)\n", result.ChangedCoverage, result.ChangedStatements, target)
}
if len(result.Failures) > 0 {
fmt.Fprintln(stderr, "coverage gate failed:")
@@ -171,13 +187,11 @@ func evaluate(input gateInput) gateResult {
result := gateResult{Failures: []string{}}
if !input.ChangedOnly {
result.Overall = coveragePercent(input.Overall)
baselineRounded := roundOne(input.BaselineOverall)
overallRounded := roundOne(result.Overall)
if overallRounded+input.OverallTolerance+1e-9 < baselineRounded {
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage regressed from %.1f%% to %.1f%%", baselineRounded, overallRounded))
if result.Overall+input.OverallTolerance < input.BaselineOverall {
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage regressed from %.4f%% to %.4f%%", input.BaselineOverall, result.Overall))
}
if input.EnforceOverall && overallRounded < input.Target {
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage %.1f%% is below target %.1f%%", overallRounded, input.Target))
if input.EnforceOverall && result.Overall < input.Target {
result.Failures = append(result.Failures, fmt.Sprintf("overall coverage %.4f%% is below target %.4f%%", result.Overall, input.Target))
}
}
@@ -206,8 +220,8 @@ func evaluate(input gateInput) gateResult {
result.ChangedStatements = total
if total > 0 {
result.ChangedCoverage = float64(covered) * 100 / float64(total)
if result.ChangedCoverage+1e-9 < input.Target {
result.Failures = append(result.Failures, fmt.Sprintf("changed code coverage %.1f%% is below target %.1f%%", result.ChangedCoverage, input.Target))
if result.ChangedCoverage < input.Target {
result.Failures = append(result.Failures, fmt.Sprintf("changed code coverage %.4f%% is below target %.4f%%", result.ChangedCoverage, input.Target))
}
}
return result
@@ -253,11 +267,13 @@ func readProfiles(paths []string, modulePath string) ([]coverageBlock, error) {
values = append(values, value)
}
blocks = append(blocks, coverageBlock{
File: normalizeProfilePath(match[1], modulePath),
StartLine: values[0],
EndLine: values[2],
Statements: values[4],
Count: values[5],
File: normalizeProfilePath(match[1], modulePath),
StartLine: values[0],
StartColumn: values[1],
EndLine: values[2],
EndColumn: values[3],
Statements: values[4],
Count: values[5],
})
}
err = scanner.Err()
@@ -402,7 +418,15 @@ func coveragePercent(blocks []coverageBlock) float64 {
func mergeCoverageBlocks(blocks []coverageBlock) []coverageBlock {
merged := make(map[string]coverageBlock, len(blocks))
for _, block := range blocks {
key := fmt.Sprintf("%s:%d:%d:%d", block.File, block.StartLine, block.EndLine, block.Statements)
key := fmt.Sprintf(
"%s:%d:%d:%d:%d:%d",
block.File,
block.StartLine,
block.StartColumn,
block.EndLine,
block.EndColumn,
block.Statements,
)
current, ok := merged[key]
if !ok || block.Count > current.Count {
merged[key] = block
@@ -419,5 +443,3 @@ func mergeCoverageBlocks(blocks []coverageBlock) []coverageBlock {
}
return result
}
func roundOne(value float64) float64 { return math.Round(value*10) / 10 }
+198 -4
View File
@@ -39,7 +39,7 @@ func TestRun(t *testing.T) {
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "overall coverage: 100.0%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
if !strings.Contains(stdout.String(), "overall coverage: 100.0000%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
t.Fatalf("unexpected output %q", stdout.String())
}
@@ -54,6 +54,115 @@ func TestRun(t *testing.T) {
}
}
func TestRunDefaultsToFullChangedCodeCoverage(t *testing.T) {
profile := filepath.Join(t.TempDir(), "coverage.out")
partialBody := "mode: atomic\n" +
"example.com/project/internal/a.go:10.1,12.2 9 1\n" +
"example.com/project/internal/a.go:13.1,13.2 1 0\n"
if err := os.WriteFile(profile, []byte(partialBody), 0o600); err != nil {
t.Fatal(err)
}
args := []string{
"--changed-only",
"--diff-profile", profile,
"--base-ref", "base",
"--module", "example.com/project",
}
loader := func(string) (map[string][]lineRange, error) {
return map[string][]lineRange{"internal/a.go": {{Start: 10, End: 13}}}, nil
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, loader, nil); code != 1 {
t.Fatalf("run code=%d, want 1; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
if !strings.Contains(stderr.String(), "changed code coverage 90.0000% is below target 100.0000%") {
t.Fatalf("default target was not enforced: %q", stderr.String())
}
fullBody := "mode: atomic\n" +
"example.com/project/internal/a.go:10.1,12.2 9 1\n" +
"example.com/project/internal/a.go:13.1,13.2 1 1\n"
if err := os.WriteFile(profile, []byte(fullBody), 0o600); err != nil {
t.Fatal(err)
}
stdout.Reset()
stderr.Reset()
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
t.Fatalf("100%% changed coverage code=%d; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
}
func TestRunEvaluatesBaselineProfileWithCandidateCoverageModel(t *testing.T) {
profile := filepath.Join(t.TempDir(), "coverage.out")
body := "mode: atomic\n" +
"example.com/project/internal/a.go:10.1,12.2 5 0\n" +
"example.com/project/internal/a.go:10.1,12.2 5 1\n" +
"example.com/project/internal/a.go:20.1,22.2 5 0\n"
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
args := []string{
"--overall-profile", profile,
"--baseline-profile", profile,
"--diff-profile", profile,
"--base-ref", "base",
"--module", "example.com/project",
}
loader := func(string) (map[string][]lineRange, error) {
return map[string][]lineRange{}, nil
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "overall coverage: 50.0000% (merge-base 50.0000%") {
t.Fatalf("baseline and candidate did not share one coverage model: %q", stdout.String())
}
}
func TestRunFailsClosedWhenBaselineProfileIsMissing(t *testing.T) {
profile := filepath.Join(t.TempDir(), "coverage.out")
body := "mode: atomic\nexample.com/project/internal/a.go:10.1,12.2 5 1\n"
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
args := []string{
"--overall-profile", profile,
"--baseline-profile", filepath.Join(t.TempDir(), "missing.out"),
"--diff-profile", profile,
"--base-ref", "base",
"--module", "example.com/project",
}
loader := func(string) (map[string][]lineRange, error) {
return map[string][]lineRange{}, nil
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, loader, nil); code != 2 {
t.Fatalf("missing baseline profile code=%d, want 2; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
if !strings.Contains(stderr.String(), "open coverage profile") {
t.Fatalf("missing baseline profile did not fail closed: %q", stderr.String())
}
}
func TestRunRejectsConflictingBaselineSources(t *testing.T) {
args := []string{
"--overall-profile", "candidate.out",
"--baseline-profile", "baseline.out",
"--baseline-overall", "100",
"--diff-profile", "candidate.out",
"--base-ref", "base",
"--module", "example.com/project",
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, nil, nil); code != 2 {
t.Fatalf("conflicting baseline sources code=%d, want 2; stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
if !strings.Contains(stderr.String(), "either --baseline-profile or --baseline-overall, not both") {
t.Fatalf("conflicting baseline sources were not rejected clearly: %q", stderr.String())
}
}
func TestCrossPlatformCoverageRunUnionsRepeatedOverallProfiles(t *testing.T) {
dir := t.TempDir()
uncovered := filepath.Join(dir, "uncovered.out")
@@ -81,7 +190,7 @@ func TestCrossPlatformCoverageRunUnionsRepeatedOverallProfiles(t *testing.T) {
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "overall coverage: 100.0%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
if !strings.Contains(stdout.String(), "overall coverage: 100.0000%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
t.Fatalf("repeated profiles were not unioned: %q", stdout.String())
}
}
@@ -113,7 +222,7 @@ func TestRunChangedOnlyWithBuildableScope(t *testing.T) {
if code := run(args, &stdout, &stderr, loader, buildableLoader); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if strings.Contains(stdout.String(), "overall coverage") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
if strings.Contains(stdout.String(), "overall coverage") || !strings.Contains(stdout.String(), "changed code coverage: 100.0000%") {
t.Fatalf("unexpected changed-only output %q", stdout.String())
}
@@ -143,7 +252,11 @@ func TestReadProfiles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(blocks) != 1 || blocks[0].File != "internal/a.go" || blocks[0].Statements != 3 {
if len(blocks) != 1 ||
blocks[0].File != "internal/a.go" ||
blocks[0].StartColumn != 1 ||
blocks[0].EndColumn != 2 ||
blocks[0].Statements != 3 {
t.Fatalf("blocks=%v", blocks)
}
if _, err := readProfiles([]string{filepath.Join(t.TempDir(), "missing")}, "example.com/project"); err == nil {
@@ -275,6 +388,41 @@ func TestCrossPlatformCoverageUnionsDuplicateCrossPackageCoverageBlocks(t *testi
}
}
func TestCoverageKeepsDistinctBlocksOnTheSameLine(t *testing.T) {
result := evaluate(gateInput{
Diff: []coverageBlock{
{
File: "internal/a.go",
StartLine: 10,
StartColumn: 1,
EndLine: 10,
EndColumn: 5,
Statements: 1,
Count: 1,
},
{
File: "internal/a.go",
StartLine: 10,
StartColumn: 6,
EndLine: 10,
EndColumn: 10,
Statements: 1,
Count: 0,
},
},
Changed: map[string][]lineRange{"internal/a.go": {{Start: 10, End: 10}}},
Target: 100,
ChangedOnly: true,
})
if result.ChangedCoverage != 50 || result.ChangedStatements != 2 {
t.Fatalf("same-line blocks were collapsed: %#v", result)
}
if len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0], "changed code coverage 50.0000% is below target 100.0000%") {
t.Fatalf("same-line uncovered block did not fail the gate: %v", result.Failures)
}
}
func TestEvaluateAllowsMeasurementTolerance(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{{Statements: 403, Count: 1}, {Statements: 597, Count: 0}},
@@ -288,6 +436,52 @@ func TestEvaluateAllowsMeasurementTolerance(t *testing.T) {
}
}
func TestEvaluateDefaultsToZeroOverallTolerance(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{{Statements: 403, Count: 1}, {Statements: 597, Count: 0}},
Changed: map[string][]lineRange{},
BaselineOverall: 40.4,
OverallTolerance: 0,
Target: 100,
})
if len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0], "overall coverage regressed from 40.4000% to 40.3000%") {
t.Fatalf("zero-tolerance regression failures = %v", result.Failures)
}
}
func TestEvaluateRejectsRegressionHiddenByOneDecimalRounding(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{
{Statements: 1999, Count: 1},
{Statements: 1, Count: 0},
},
Changed: map[string][]lineRange{},
BaselineOverall: 100,
Target: 100,
})
if len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0], "overall coverage regressed from 100.0000% to 99.9500%") {
t.Fatalf("sub-tenth regression failures = %v", result.Failures)
}
}
func TestEvaluateRejectsChangedCoverageBelow100WithoutRounding(t *testing.T) {
result := evaluate(gateInput{
Diff: []coverageBlock{
{File: "internal/a.go", StartLine: 1, EndLine: 1999, Statements: 1999, Count: 1},
{File: "internal/a.go", StartLine: 2000, EndLine: 2000, Statements: 1, Count: 0},
},
Changed: map[string][]lineRange{"internal/a.go": {{Start: 1, End: 2000}}},
Target: 100,
ChangedOnly: true,
})
if len(result.Failures) != 1 ||
!strings.Contains(result.Failures[0], "changed code coverage 99.9500% is below target 100.0000%") {
t.Fatalf("sub-100 changed-code failures = %v", result.Failures)
}
}
func TestEvaluateFailsClosed(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{
-4
View File
@@ -20,8 +20,6 @@ type docsFlagCase struct {
}
func TestDWSDocsCommandTreeCoverage(t *testing.T) {
t.Parallel()
docPaths, _, err := parseDWSDocsCompatibility()
if err != nil {
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
@@ -45,8 +43,6 @@ func TestDWSDocsCommandTreeCoverage(t *testing.T) {
}
func TestDWSDocsLocalFlagsCoverage(t *testing.T) {
t.Parallel()
docPaths, docFlags, err := parseDWSDocsCompatibility()
if err != nil {
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
-4
View File
@@ -14,8 +14,6 @@ import (
// and their availability depends on the test environment's fixture data.
func TestHiddenMCPHelpIsReachable(t *testing.T) {
t.Parallel()
cmd := app.NewRootCommand()
var out strings.Builder
cmd.SetOut(&out)
@@ -33,8 +31,6 @@ func TestHiddenMCPHelpIsReachable(t *testing.T) {
}
func TestSkillCommandIsRegistered(t *testing.T) {
t.Parallel()
root := app.NewRootCommand()
var found bool
+2
View File
@@ -11,6 +11,8 @@ import (
func TestMain(m *testing.M) {
// Set an empty catalog fixture so that EnvironmentLoader does not
// attempt live discovery (which would hang on unreachable MCP endpoints).
// Tests that construct app root commands must remain serial because root
// construction initializes process-wide helper dependencies.
absFixture, _ := filepath.Abs("testdata/empty_catalog.json")
os.Setenv(cli.CatalogFixtureEnv, absFixture)
@@ -8,8 +8,6 @@ import (
)
func TestRootHelpHidesRecoveredModules(t *testing.T) {
t.Parallel()
cmd := app.NewRootCommand()
var out strings.Builder
cmd.SetOut(&out)
+1
View File
@@ -24,6 +24,7 @@ func TestAIBehaviorPolicyProtectsEnforcementInputs(t *testing.T) {
workflow := string(data)
for _, want := range []string{
"filename.startsWith('scripts/ci/')",
"filename.startsWith('scripts/policy/')",
"filename === 'test/fixtures/cli-interface-baseline.txt'",
"previous_filename",
+34 -1
View File
@@ -401,7 +401,7 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
admission := readWorkflow(".github/workflows/ci.yml")
for _, want := range []string{
"name: Code Admission — PR 合入门禁",
"name: CI",
"files.length === 1",
"files[0].filename === 'CHANGELOG.md'",
"files[0].status === 'modified'",
@@ -417,6 +417,14 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
"mode=--fast-path",
`"$mode" "$PR_BASE_SHA" HEAD`,
"needs.lint.outputs.platform_sensitive == 'true'",
`COVERAGE_TARGET: "100"`,
`COVERAGE_ENFORCE_OVERALL: "false"`,
`COVERAGE_OVERALL_TOLERANCE: "0"`,
`COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt`,
"run: make test-plan",
"run: make format-check",
"./scripts/ci/test-packages.sh list",
`package_output="$(./scripts/ci/test-packages.sh list`,
} {
if !strings.Contains(admission, want) {
t.Errorf("Code Admission workflow missing contract %q", want)
@@ -451,6 +459,31 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
t.Error("Code Admission must not suppress required contexts with paths-ignore")
}
notification := readWorkflow(".github/workflows/notify-wukong.yml")
if !strings.Contains(notification, "- CI") {
t.Error("Wukong notification must follow the renamed CI workflow")
}
if strings.Contains(notification, "Code Admission — PR 合入门禁") {
t.Error("Wukong notification still follows the retired workflow display name")
}
coverageGate := readWorkflow("scripts/policy/check-coverage-gate.sh")
if !strings.Contains(coverageGate, `TARGET="${COVERAGE_TARGET:-100}"`) {
t.Error("coverage gate must default to 100% changed-code coverage")
}
if !strings.Contains(coverageGate, `OVERALL_TOLERANCE="${COVERAGE_OVERALL_TOLERANCE:-0}"`) {
t.Error("coverage gate must reject any reported overall regression")
}
if !strings.Contains(coverageGate, `--baseline-profile "$BASELINE_PROFILE"`) {
t.Error("coverage gate must evaluate the merge-base profile with the candidate checker")
}
if strings.Contains(coverageGate, `--overall-profile "$ADDITIONAL_DIFF_PROFILE"`) {
t.Error("supporting changed-code coverage must not inflate candidate overall coverage")
}
if strings.Contains(coverageGate, `go tool cover -func="$BASELINE_PROFILE"`) {
t.Error("coverage baseline must not use a different coverage calculator")
}
aiBehavior := readWorkflow(".github/workflows/ai-behavior-check.yml")
for _, want := range []string{
"name: Code Admission — AI Behavior",
+106
View File
@@ -0,0 +1,106 @@
package scripts_test
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestCITestPackagePlanCoversDefaultPackagesExactlyOnce(t *testing.T) {
root := testPackagePlanRoot(t)
output := runTestPackagePlan(t, root, "verify")
if !strings.Contains(output, "default packages exactly once") {
t.Fatalf("verify output = %q, want coverage summary", output)
}
}
func TestCITestPackagePlanRoutesPublicTestSuites(t *testing.T) {
root := testPackagePlanRoot(t)
remaining := strings.Fields(runTestPackagePlan(t, root, "list", "remaining"))
releaseScripts := strings.Fields(runTestPackagePlan(t, root, "list", "release-scripts"))
for _, suffix := range []string{
"/test/cli",
"/test/contract",
"/test/integration/extensions",
"/test/mock_mcp",
"/test/smoke",
"/test/unit",
} {
if !containsPackageSuffix(remaining, suffix) {
t.Errorf("remaining shard does not contain package ending in %q", suffix)
}
}
if containsPackageSuffix(remaining, "/test/scripts") {
t.Error("remaining shard unexpectedly contains /test/scripts")
}
if !containsPackageSuffix(releaseScripts, "/test/scripts") {
t.Error("release-scripts shard does not contain /test/scripts")
}
}
func TestCITestPackagePlanFailsClosedWhenGoListFails(t *testing.T) {
root := testPackagePlanRoot(t)
fakeBin := t.TempDir()
fakeGo := filepath.Join(fakeBin, "go")
err := os.WriteFile(fakeGo, []byte(`#!/bin/sh
if [ "$1" = "list" ] && [ "$2" = "-m" ]; then
printf '%s\n' 'github.com/DingTalk-Real-AI/dingtalk-workspace-cli'
exit 0
fi
printf '%s\n' 'injected go list failure' >&2
exit 42
`), 0o755)
if err != nil {
t.Fatalf("write fake go: %v", err)
}
script := filepath.Join(root, "scripts", "ci", "test-packages.sh")
for _, args := range [][]string{{"list", "remaining"}, {"verify"}} {
cmd := exec.Command("sh", append([]string{script}, args...)...)
cmd.Dir = root
cmd.Env = []string{
"PATH=" + fakeBin + string(os.PathListSeparator) + os.Getenv("PATH"),
"TMPDIR=" + t.TempDir(),
}
output, runErr := cmd.CombinedOutput()
if runErr == nil {
t.Fatalf("%s unexpectedly succeeded with failing go list:\n%s", strings.Join(args, " "), output)
}
if !strings.Contains(string(output), "injected go list failure") {
t.Fatalf("%s failure output = %q, want injected failure", strings.Join(args, " "), output)
}
}
}
func testPackagePlanRoot(t *testing.T) string {
t.Helper()
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("resolve repository root: %v", err)
}
return root
}
func runTestPackagePlan(t *testing.T, root string, args ...string) string {
t.Helper()
script := filepath.Join(root, "scripts", "ci", "test-packages.sh")
cmd := exec.Command("sh", append([]string{script}, args...)...)
cmd.Dir = root
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %s failed: %v\n%s", script, strings.Join(args, " "), err, output)
}
return string(output)
}
func containsPackageSuffix(packages []string, suffix string) bool {
for _, packagePath := range packages {
if strings.HasSuffix(packagePath, suffix) {
return true
}
}
return false
}
-234
View File
@@ -1,234 +0,0 @@
#!/usr/bin/env bash
# run_all_tests.sh — Run all test suites in parallel and print a summary.
# Usage: bash test/scripts/run_all_tests.sh [--verbose] [--timeout <duration>] [--jobs <n>]
#
# Options:
# --verbose Print full test output for every suite (default: only on failure)
# --timeout <dur> Per-suite go test timeout (default: 120s)
# --jobs <n> Max parallel suites (default: number of CPU cores)
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
VERBOSE=false
TIMEOUT="120s"
MAX_JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
# ── Parse flags ──────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--verbose) VERBOSE=true; shift ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--jobs) MAX_JOBS="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# ── Colour helpers ────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# ── Test suite definitions ────────────────────────────────────────────────────
# Each entry: "label|package_path"
declare -a SUITES=(
"cli|./test/cli/..."
"contract|./test/contract/..."
"integration/extensions|./test/integration/extensions/..."
"integration/recovery|./test/integration/recovery/..."
"mock_mcp|./test/mock_mcp/..."
"scripts|./test/scripts/..."
)
# ── Temporary workspace ───────────────────────────────────────────────────────
LOG_DIR="$(mktemp -d)"
trap 'rm -rf "$LOG_DIR"' EXIT
# ── Worker: run one suite, write result files ─────────────────────────────────
# Result files written to $LOG_DIR/<safe_label>/{status,output}
run_suite_worker() {
local label="$1"
local pkg="$2"
local safe_label="${label//\//_}"
local suite_dir="$LOG_DIR/$safe_label"
mkdir -p "$suite_dir"
# Check whether the package directory actually exists
local pkg_dir="${pkg//\.\//}"
pkg_dir="${pkg_dir%/...}"
if [[ ! -d "$REPO_ROOT/$pkg_dir" ]]; then
echo "skip" > "$suite_dir/status"
echo "(directory not found)" > "$suite_dir/output"
return
fi
local exit_code=0
(
cd "$REPO_ROOT"
go test -timeout "$TIMEOUT" -count=1 "$pkg" 2>&1
) > "$suite_dir/output" 2>&1 || exit_code=$?
if [[ $exit_code -eq 0 ]]; then
echo "pass" > "$suite_dir/status"
elif grep -q "matched no packages\|no packages to test\|\[no test files\]" "$suite_dir/output" 2>/dev/null; then
echo "skip" > "$suite_dir/status"
echo "(no test files found)" > "$suite_dir/output"
else
echo "fail" > "$suite_dir/status"
fi
}
export -f run_suite_worker
export REPO_ROOT TIMEOUT LOG_DIR
# ── Main ──────────────────────────────────────────────────────────────────────
echo ""
printf "${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}\n"
printf "${BOLD}║ dws — Full Test Suite Runner ║${RESET}\n"
printf "${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}\n"
echo ""
printf "Repo root : %s\n" "$REPO_ROOT"
printf "Timeout : %s per suite\n" "$TIMEOUT"
printf "Parallel : %s jobs\n" "$MAX_JOBS"
printf "Verbose : %s\n" "$VERBOSE"
echo ""
printf "${BOLD}Launching %d suites in parallel…${RESET}\n" "${#SUITES[@]}"
echo ""
START_TIME=$(date +%s)
# ── Dispatch all suites concurrently with a job-slot semaphore ────────────────
declare -a PIDS=()
declare -a PID_LABELS=()
active_jobs=0
for suite in "${SUITES[@]}"; do
label="${suite%%|*}"
pkg="${suite##*|}"
printf " ${CYAN}START${RESET} %s\n" "$label"
run_suite_worker "$label" "$pkg" &
PIDS+=($!)
PID_LABELS+=("$label")
active_jobs=$((active_jobs + 1))
# Throttle: wait for one slot to free up when at capacity
if [[ $active_jobs -ge $MAX_JOBS ]]; then
wait "${PIDS[$((${#PIDS[@]} - MAX_JOBS))]}" 2>/dev/null || true
active_jobs=$((active_jobs - 1))
fi
done
# Wait for all remaining background jobs
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
# ── Collect results in original suite order ───────────────────────────────────
TOTAL=0
PASSED=0
FAILED=0
SKIPPED=0
declare -a FAILED_SUITES=()
declare -a PASSED_SUITES=()
declare -a SKIPPED_SUITES=()
echo ""
printf "${BOLD}Results:${RESET}\n"
echo ""
for suite in "${SUITES[@]}"; do
label="${suite%%|*}"
safe_label="${label//\//_}"
suite_dir="$LOG_DIR/$safe_label"
status_file="$suite_dir/status"
output_file="$suite_dir/output"
TOTAL=$((TOTAL + 1))
if [[ ! -f "$status_file" ]]; then
# Worker never wrote a status — treat as failure
printf " ${RED}FAIL${RESET} %s (worker did not complete)\n" "$label"
FAILED=$((FAILED + 1))
FAILED_SUITES+=("$label")
continue
fi
status="$(cat "$status_file")"
case "$status" in
pass)
PASSED=$((PASSED + 1))
PASSED_SUITES+=("$label")
printf " ${GREEN}PASS${RESET} %s\n" "$label"
if $VERBOSE; then
sed 's/^/ /' "$output_file"
fi
;;
skip)
SKIPPED=$((SKIPPED + 1))
SKIPPED_SUITES+=("$label")
printf " ${YELLOW}SKIP${RESET} %s %s\n" "$label" "$(cat "$output_file")"
;;
fail)
FAILED=$((FAILED + 1))
FAILED_SUITES+=("$label")
printf " ${RED}FAIL${RESET} %s\n" "$label"
echo " ── output ──────────────────────────────────────────────────────"
sed 's/^/ /' "$output_file"
echo " ────────────────────────────────────────────────────────────────"
;;
esac
done
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
printf "${BOLD} Summary (elapsed: %ds, parallel: %s jobs)${RESET}\n" "$ELAPSED" "$MAX_JOBS"
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
printf " Total : %d\n" "$TOTAL"
printf " ${GREEN}Passed : %d${RESET}\n" "$PASSED"
printf " ${RED}Failed : %d${RESET}\n" "$FAILED"
printf " ${YELLOW}Skipped : %d${RESET}\n" "$SKIPPED"
echo ""
if [[ ${#FAILED_SUITES[@]} -gt 0 ]]; then
printf "${RED}${BOLD}Failed suites:${RESET}\n"
for suite in "${FAILED_SUITES[@]}"; do
printf " ${RED}✗${RESET} %s\n" "$suite"
done
echo ""
fi
if [[ ${#SKIPPED_SUITES[@]} -gt 0 ]]; then
printf "${YELLOW}Skipped suites:${RESET}\n"
for suite in "${SKIPPED_SUITES[@]}"; do
printf " ${YELLOW}–${RESET} %s\n" "$suite"
done
echo ""
fi
if [[ ${#PASSED_SUITES[@]} -gt 0 ]]; then
printf "${GREEN}Passed suites:${RESET}\n"
for suite in "${PASSED_SUITES[@]}"; do
printf " ${GREEN}✓${RESET} %s\n" "$suite"
done
echo ""
fi
# ── Exit code ─────────────────────────────────────────────────────────────────
if [[ $FAILED -gt 0 ]]; then
printf "${RED}${BOLD}Result: FAILED${RESET}\n\n"
exit 1
else
printf "${GREEN}${BOLD}Result: ALL PASSED${RESET}\n\n"
exit 0
fi