Compare commits

..
Author SHA1 Message Date
chichuan 1a6ae856ec Merge branch 'main' into ci-coverage-speedup 2026-08-14 18:16:14 +08:00
chichuan 9c6407ae74 ci: align baseline coverage cache paths 2026-08-14 18:06:49 +08:00
github-actions[bot] b9b8cc2c77 Merge pull request #954 from xlb1130/fix/85200556-im-id-flags-v3
fix(chat): converge IM ID flags
2026-08-14 09:44:45 +00:00
xlb1130 e02fdbdc8f Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 17:29:22 +08:00
github-actions[bot] ce529c9337 chore: update beta formula for v1.0.59-beta.1 [skip ci] 2026-08-14 09:20:43 +00:00
xlb1130 6952b22f45 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 16:39:56 +08:00
chichuan 3aa06e32fa ci: shard full-suite coverage and cache merge-base profile
The Coverage context was the PR critical path (~17 min end to end):
coverage-current re-ran the whole suite serially (-p 1, ~13 min) and
coverage-baseline re-ran it again at the merge-base (~13 min) although
that profile is a pure function of the base commit.

- coverage-current now owns only the scoped (standard-tier) profile;
  full-suite candidate profiles come from a 5-way shard matrix
  (app/cli/generators/helpers/remaining) that keeps -p 1 inside each
  shard on isolated runners. scripts/ci/test-packages.sh list-coverage
  defines the shards and verify proves the union equals the previous
  single-run package set exactly once.
- the aggregate Coverage job reassembles the disjoint shard profiles
  into coverage.txt before make coverage-gate, failing closed when a
  shard file is missing, so gate semantics (100% changed-code +
  scope-matched overall non-regression) are byte-compatible.
- coverage-baseline restores the merge-base full-suite profile from an
  exact-key cache (merge-base SHA + resolved Go version) written by the
  last green main push; any miss falls back to recomputing in the
  merge-base worktree. Exact key only - no prefix fallback, a near-miss
  profile would compare the candidate against the wrong commit.
- new contract tests pin the shard matrix, the assembly step, the
  exact-key cache pair, and the absence of restore-keys; the package
  plan test also covers the coverage shard partition.
2026-08-14 16:24:00 +08:00
xlb1130 b17e030d1f Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 15:42:27 +08:00
xlb1130 afd8422580 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 15:16:17 +08:00
长真 a7a0a97115 test(chat): align open id fixtures with current format 2026-08-14 12:25:07 +08:00
xlb1130 cbaa8c9bf5 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 11:39:54 +08:00
长真 0f5ecb609b fix(cli): restore audit join user guard 2026-08-14 11:38:26 +08:00
长真 d0e6aba319 fix(cli): cover alias exclude guard branches 2026-08-14 00:23:18 +08:00
xlb1130 b0b18986b1 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-14 00:10:33 +08:00
xlb1130 abecb0dee1 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 23:28:09 +08:00
长真 d17f50b9de fix(cli): keep real flags out of alias blocked list 2026-08-13 23:26:08 +08:00
xlb1130 10417396f1 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 22:15:34 +08:00
xlb1130 410a63ea9a Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 20:59:34 +08:00
长真 58c382efb7 Merge remote-tracking branch 'origin/fix/85200556-im-id-flags-v3' into fix/85200556-im-id-flags-v3 2026-08-13 20:57:29 +08:00
长真 3598586bc0 fix(cli): block plural id flag normalization 2026-08-13 20:56:43 +08:00
xlb1130 b066a14f0c Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 20:35:07 +08:00
xlb1130 94d4b5dcc9 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 18:58:13 +08:00
长真 5cbf18713a docs(changes): expand chat im flag migration note 2026-08-13 18:57:44 +08:00
长真 78d94380e7 docs(changes): note chat im id flag migration 2026-08-13 18:54:00 +08:00
xlb1130 c718b051c2 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 16:34:16 +08:00
xlb1130 6a93f14e0a Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 16:29:35 +08:00
长真 913b7cf9a9 chore(cli): refresh generated param aliases 2026-08-13 16:18:11 +08:00
xlb1130 a354144412 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 15:40:39 +08:00
长真 d525648b45 fix(chat): support read-status conversation aliases 2026-08-13 15:24:27 +08:00
长真 f3f1174407 chore(ci): rerun pr checks 2026-08-13 13:36:42 +08:00
xlb1130 ce6d5fb538 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 13:11:23 +08:00
长真 43882bf959 fix(chat): preserve schema compatibility for im flags 2026-08-13 13:02:34 +08:00
xlb1130 55c6a09bbc Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 11:58:53 +08:00
xlb1130 286376df93 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-13 10:54:21 +08:00
xlb1130 b8deec9087 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-12 23:47:39 +08:00
长真 dbee2de1d5 fix(chat): align im id flag migration scope 2026-08-12 23:45:05 +08:00
长真 a55bd9bff8 fix(chat): complete pending id flag migrations 2026-08-12 20:53:53 +08:00
xlb1130 516bd5d99c Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-12 20:09:09 +08:00
长真 65a00b497b fix(chat): migrate audit join validation id flag 2026-08-12 20:06:09 +08:00
长真 3e4a3fb9d9 Merge remote-tracking branch 'origin/fix/85200556-im-id-flags-v3' into fix/85200556-im-id-flags-v3 2026-08-12 18:06:56 +08:00
长真 1f1c27d68f fix(chat): restore audit join group flag 2026-08-12 18:06:10 +08:00
xlb1130 1b8ca149cb Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-12 17:03:29 +08:00
长真 b6c508acdf fix(chat): canonicalize send-card id flags 2026-08-12 11:49:14 +08:00
xlb1130 9472f4a1d9 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-12 10:48:44 +08:00
xlb1130 90e27c4b86 Merge branch 'main' into fix/85200556-im-id-flags-v3 2026-08-12 10:41:36 +08:00
长真 132dea9aaa fix(chat): hide remaining im id aliases 2026-08-11 22:51:16 +08:00
长真 5034c332fe fix(chat): converge im id flags 2026-08-11 22:38:37 +08:00
26 changed files with 2181 additions and 885 deletions
+7
View File
@@ -0,0 +1,7 @@
---
category: Changed
---
- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags.
- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces.
- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name.
+173 -38
View File
@@ -911,7 +911,7 @@ jobs:
coverage-current:
name: Coverage (current)
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' }}
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -926,10 +926,6 @@ jobs:
with:
go-version-file: go.mod
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Resolve authoritative coverage base
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -951,41 +947,33 @@ jobs:
- name: Build
run: make build
- name: Run current unit tests with coverage
- name: Run scoped unit tests with coverage
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
run: |
set -euo pipefail
if [ "$FULL_SUITE" = true ]; then
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
echo "No buildable Go package needs scoped coverage." \
>> "$GITHUB_STEP_SUMMARY"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 -p 1 \
-coverpkg="$coverpkg" \
-coverprofile=coverage.txt \
-covermode=atomic \
./ ./cmd/... ./internal/... ./skills/...
else
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
echo "No buildable Go package needs scoped coverage." \
>> "$GITHUB_STEP_SUMMARY"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 -p 1 \
-coverpkg="$coverpkg" \
-coverprofile=coverage.txt \
-covermode=atomic \
"${impacted_packages[@]}"
fi
"${impacted_packages[@]}"
fi
if [ "$(wc -l < coverage.txt)" -gt 1 ]; then
go tool cover -func=coverage.txt
@@ -998,6 +986,66 @@ jobs:
path: coverage.txt
retention-days: 1
coverage-current-full:
name: "Coverage (current: ${{ matrix.shard }})"
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard:
- app
- cli
- generators
- helpers
- remaining
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install archive tooling
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Build
run: make build
# Each shard keeps -p 1 so the authoritative measurement stays serial
# inside one instrumented process group; shards run on isolated runners,
# and scripts/ci/test-packages.sh verify proves the shard union equals
# the previous single full-suite package set exactly once.
- name: Run current shard tests with coverage
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
COVERAGE_SHARD: ${{ matrix.shard }}
run: |
set -euo pipefail
package_output="$(./scripts/ci/test-packages.sh list-coverage "$COVERAGE_SHARD")"
test -n "$package_output"
mapfile -t packages <<< "$package_output"
test "${#packages[@]}" -gt 0
go test -count=1 -p 1 \
-coverprofile="coverage-shard-$COVERAGE_SHARD.txt" \
-covermode=atomic \
"${packages[@]}"
go tool cover -func="coverage-shard-$COVERAGE_SHARD.txt" | tail -n 1
- name: Upload current shard coverage profile
uses: actions/upload-artifact@v4
with:
name: coverage-current-shard-${{ matrix.shard }}
path: coverage-shard-${{ matrix.shard }}.txt
retention-days: 1
coverage-supporting:
name: Coverage (supporting)
needs: lint
@@ -1051,14 +1099,11 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
id: setup-go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Resolve authoritative coverage base
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
@@ -1076,7 +1121,34 @@ jobs:
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV"
# The merge-base full-suite profile is a pure function of the base
# commit. Reuse the profile published by the last green push run of
# exactly that commit instead of re-running the whole suite; any key
# mismatch falls back to authoritative recomputation. Exact key only,
# never prefix fallback: a near-miss profile would compare the
# candidate against the wrong commit.
- name: Restore cached merge-base coverage profile
id: baseline-cache
if: needs.lint.outputs.full_suite == 'true'
uses: actions/cache/restore@v4
with:
path: coverage-cache.txt
key: dws-coverage-full-v2-${{ env.COVERAGE_BASE_REF }}-go${{ steps.setup-go.outputs.go-version }}
- name: Materialize cached merge-base coverage profile
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit == 'true'
run: |
set -eu
test -s coverage-cache.txt
test "$(head -n 1 coverage-cache.txt)" = "mode: atomic"
cp coverage-cache.txt coverage-base.txt
- name: Install archive tooling
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit != 'true'
run: sudo apt-get update && sudo apt-get install -y zip unzip
- name: Run baseline unit tests with coverage
if: steps.baseline-cache.outputs.cache-hit != 'true'
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
@@ -1125,6 +1197,21 @@ jobs:
fi
)
- name: Prepare merge-base coverage profile cache
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit != 'true'
run: |
set -eu
test -s coverage-base.txt
test "$(head -n 1 coverage-base.txt)" = "mode: atomic"
cp coverage-base.txt coverage-cache.txt
- name: Save merge-base coverage profile cache
if: needs.lint.outputs.full_suite == 'true' && steps.baseline-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: coverage-cache.txt
key: dws-coverage-full-v2-${{ env.COVERAGE_BASE_REF }}-go${{ steps.setup-go.outputs.go-version }}
- name: Upload baseline coverage profile
uses: actions/upload-artifact@v4
with:
@@ -1137,6 +1224,7 @@ jobs:
needs:
- lint
- coverage-current
- coverage-current-full
- coverage-supporting
- coverage-baseline
- coverage-darwin
@@ -1152,6 +1240,7 @@ jobs:
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
PLATFORM_SENSITIVE: ${{ needs.lint.outputs.platform_sensitive }}
CURRENT_RESULT: ${{ needs.coverage-current.result }}
CURRENT_FULL_RESULT: ${{ needs.coverage-current-full.result }}
SUPPORTING_RESULT: ${{ needs.coverage-supporting.result }}
BASELINE_RESULT: ${{ needs.coverage-baseline.result }}
DARWIN_RESULT: ${{ needs.coverage-darwin.result }}
@@ -1159,6 +1248,7 @@ jobs:
run: |
failed=0
current_expected=success
current_full_expected=skipped
supporting_expected=skipped
baseline_expected=success
native_expected=skipped
@@ -1166,6 +1256,8 @@ jobs:
current_expected=skipped
baseline_expected=skipped
elif [ "$FULL_SUITE" = true ]; then
current_expected=skipped
current_full_expected=success
supporting_expected=success
fi
if [ "$CHANGELOG_ONLY" != true ] &&
@@ -1176,6 +1268,7 @@ jobs:
for profile in \
"current:$CURRENT_RESULT:$current_expected" \
"current shards:$CURRENT_FULL_RESULT:$current_full_expected" \
"supporting:$SUPPORTING_RESULT:$supporting_expected" \
"baseline:$BASELINE_RESULT:$baseline_expected"
do
@@ -1211,6 +1304,7 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Set up Go
id: setup-go
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
uses: actions/setup-go@v5
with:
@@ -1234,11 +1328,12 @@ jobs:
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV"
- name: Download current coverage profile
- name: Download current coverage profiles
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
uses: actions/download-artifact@v4
with:
name: coverage-current-profile
pattern: coverage-current-*
merge-multiple: true
path: .
- name: Download supporting coverage profiles
@@ -1255,6 +1350,26 @@ jobs:
name: coverage-baseline-profile
path: .
# Shard profiles cover disjoint package sets, so their block-level
# concatenation is the same candidate profile one serial run produced.
# Every expected shard must be present; a missing shard would silently
# shrink the scope-matched overall comparison.
- name: Assemble full-suite coverage profile
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
shell: bash
run: |
set -euo pipefail
test ! -f coverage.txt
for shard in app cli generators helpers remaining; do
profile="coverage-shard-$shard.txt"
test -f "$profile"
test "$(head -n 1 "$profile")" = "mode: atomic"
done
printf 'mode: atomic\n' > coverage.txt
for shard in app cli generators helpers remaining; do
tail -n +2 "coverage-shard-$shard.txt" >> coverage.txt
done
- name: Enforce coverage gate
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
env:
@@ -1275,6 +1390,26 @@ jobs:
COVERAGE_ADDITIONAL_DIFF_PROFILE="$additional_profile" \
make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
# Publish this push's full-suite profile as the merge-base cache for
# future PRs whose merge-base is exactly this commit. Saved only after
# the gate passed so a broken run never becomes a baseline. Both producer
# and consumer use coverage-cache.txt because the cache version includes
# the configured path as well as the compression tool.
- name: Prepare push coverage profile as merge-base cache
if: github.event_name == 'push' && needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
run: |
set -eu
test -s coverage.txt
test "$(head -n 1 coverage.txt)" = "mode: atomic"
cp coverage.txt coverage-cache.txt
- name: Save push coverage profile as merge-base cache
if: github.event_name == 'push' && needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
uses: actions/cache/save@v4
with:
path: coverage-cache.txt
key: dws-coverage-full-v2-${{ github.sha }}-go${{ steps.setup-go.outputs.go-version }}
- name: Generate coverage report
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
run: |
+11 -11
View File
@@ -1,33 +1,33 @@
class DingtalkWorkspaceCliBeta < Formula
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
version "1.0.58-beta.6"
version "1.0.59-beta.1"
license "Apache-2.0"
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-darwin-arm64.tar.gz"
sha256 "8f55497b84113f81b318e087c723016a02eded1cb5784cbd0811fe527d5852ca"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-darwin-arm64.tar.gz"
sha256 "36a30f3496e0f759c15c0b09f67dbd23b8ecdfff2eebe572f88125b26485830f"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-darwin-amd64.tar.gz"
sha256 "b1762f1640310fb4100634fe54d9baace46384bb270c59148fe306275554d491"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-darwin-amd64.tar.gz"
sha256 "e7a04906380efd8da88cd112e6a512bb6470a3956dc370150037ed6e314db445"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-linux-arm64.tar.gz"
sha256 "55393310ef0e1f24ea2c0dc22f00c0eb3b343edc2deb18d0db7838cda65af25d"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-linux-arm64.tar.gz"
sha256 "f59ab055f3e841e4cebc964ae3ef969668475548abaaf8bede44afdca9a3e28d"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-linux-amd64.tar.gz"
sha256 "3830f77d09b4da4aa39f0c08d772ff3fa1ffb837eb15bb417f97edbccb073b7d"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-linux-amd64.tar.gz"
sha256 "2c8f919489d958c7d49262615e81faac70a9fbcae2d589ab54a0bb3c5700a057"
end
end
resource "skills" do
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.6/dws-skills.zip"
sha256 "f304a883a4f9e938b26a44692cd5a8d3d8704ba70ee7f33ba7c288434da72b6f"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-skills.zip"
sha256 "25f4a7e1d01fa4d771d79201b34b11ee8a24182bdcdc94bfb98d2bd5845bed3b"
end
def install
+1 -1
View File
@@ -18,7 +18,7 @@ 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 test-plan - Verify every default Go package belongs to one CI test shard\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 lint - Run formatting checks, go vet, and staticcheck\n"
@printf " make format-check - Check all repository Go source files with gofmt\n"
+15 -2
View File
@@ -201,8 +201,21 @@ base_ref=$(git merge-base HEAD origin/main)
standard PR, CI derives changed packages and their reverse-dependency test
closure, then generates candidate and merge-base profiles with the same test
scope and `coverpkg`. High-risk and protected-main runs use the complete
profiles. Supporting and (when platform-selected) native profiles are
generated before the aggregate `Coverage` context evaluates them. The
profiles. The complete candidate profile is produced by disjoint per-shard
helper jobs (`scripts/ci/test-packages.sh list-coverage`, kept serial with
`-p 1` inside each shard; `verify` proves the shard union equals the
full-suite scope exactly once) and concatenated in the aggregate job before
enforcement. The complete merge-base profile is restored from an exact-key
cache written by the last green `main` push of that same commit (key:
merge-base SHA plus resolved Go version); any miss falls back to recomputing
it in a merge-base worktree. The trusted `main` producer and PR consumer use
the same dedicated cache profile path because GitHub includes that path in the
cache version; the runtime-facing candidate and baseline filenames remain
separate. Near-miss reuse is forbidden — the caches carry no prefix restore
keys, because a neighbouring commit's profile would compare the candidate
against the wrong baseline. Supporting and (when
platform-selected) native profiles are generated 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,
scope-matched merge-base non-regression check. Candidate and baseline profiles
+5 -1
View File
@@ -479,7 +479,11 @@ func TestCrossPlatformCoverageChatReactionConversationAliasesReachCanonicalPaylo
if err != nil {
t.Fatalf("alias execution failed: %v", err)
}
if ctx == nil || len(ctx.Corrections) != 1 || ctx.Corrections[0].Original != "--"+alias || ctx.Corrections[0].Corrected != "--conversation-id" {
if alias == "open-conversation-id" {
if ctx == nil || len(ctx.Corrections) != 0 {
t.Fatalf("alias corrections = %#v", ctx)
}
} else if ctx == nil || len(ctx.Corrections) != 1 || ctx.Corrections[0].Original != "--"+alias || ctx.Corrections[0].Corrected != "--conversation-id" {
t.Fatalf("alias corrections = %#v", ctx)
}
if !reflect.DeepEqual(aliasCaller.calls, canonicalCaller.calls) {
+2 -2
View File
@@ -94,8 +94,8 @@ func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) {
}{
{args: []string{"chat", "send", "--group", "cid-stable", "--text", "hello"}, hint: "dws chat message send"},
{args: []string{"im", "send", "--group", "cid-stable", "--text", "hello"}, hint: "dws chat message send"},
{args: []string{"chat", "history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"},
{args: []string{"im", "history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"},
{args: []string{"chat", "history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --conversation-id <GROUP_OPEN_CONVERSATION_ID>"},
{args: []string{"im", "history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --conversation-id <GROUP_OPEN_CONVERSATION_ID>"},
} {
command := NewRootCommand()
command.SilenceErrors = true
@@ -207,7 +207,11 @@ func assertChatCatalogCompleteLeafContracts(t testing.TB) {
})
auditJoin := executeShortcutSchemaQuery(t, "--cli-path", "chat group audit-join-validation")
assertSchemaLeafParameterRequired(t, auditJoin, "chat group audit-join-validation", "conversation-id", true)
assertSchemaLeafParameterEnum(t, auditJoin, "chat group audit-join-validation", "status", []string{"AuditApprove", "AuditDelete"})
if parameters := schemaContractMap(auditJoin["parameters"]); parameters["group"] != nil {
t.Fatalf("chat group audit-join-validation publishes hidden --group alias: %#v", parameters["group"])
}
}
func assertSchemaLeafParameterRequired(t testing.TB, leaf map[string]any, cliPath, name string, want bool) {
+18 -12
View File
@@ -177,6 +177,7 @@ func reduceLeafParamAliases(path string, realByMorph map[string][]realFlag, conc
aliasMap := make(map[string]string)
blockedSet := make(map[string]bool)
excludedSet := make(map[string]bool)
claimedRealSet := make(map[string]bool)
pendingReview := ov.Confirm || ov.Investigate
for boundFlag, conceptID := range ov.Bind {
@@ -218,6 +219,14 @@ func reduceLeafParamAliases(path string, realByMorph map[string][]realFlag, conc
if len(candidates) == 0 {
continue
}
for m := range eff {
if _, isReal := realByMorph[m]; isReal {
claimedRealSet[m] = true
}
}
for _, exclude := range concept.Excludes {
excludedSet[cmdutil.Morph(exclude)] = true
}
visible := distinctRealNames(candidates, true)
var canon string
switch len(visible) {
@@ -267,21 +276,18 @@ func reduceLeafParamAliases(path string, realByMorph map[string][]realFlag, conc
}
aliasMap[m] = canon
}
// Excludes are not passive prose: once this concept is active on a
// reviewed command, a non-real excluded spelling is protected from
// downstream fuzzy correction. A real flag is left alone because it
// already has an independently valid command-local meaning.
for _, exclude := range concept.Excludes {
morphed := cmdutil.Morph(exclude)
if _, isReal := realByMorph[morphed]; !isReal {
excludedSet[morphed] = true
}
}
}
for excluded := range excludedSet {
if _, isAlias := aliasMap[excluded]; !isAlias {
blockedSet[excluded] = true
if _, isAlias := aliasMap[excluded]; isAlias {
continue
}
if claimedRealSet[excluded] {
continue
}
if _, isReal := realByMorph[excluded]; isReal {
continue
}
blockedSet[excluded] = true
}
// (b) Command scoped aliases override concept reductions.
+146 -265
View File
@@ -889,13 +889,9 @@ var generatedParamAliases = []ParamAliasEntry{
Blocked: []string{"cursor"},
},
{
CLIPath: "chat category add-conv",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"category-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
CLIPath: "chat category add-conv",
Blocked: []string{"category-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat", "chat-id", "open-conversation-id"},
},
{
CLIPath: "chat category create",
@@ -920,13 +916,9 @@ var generatedParamAliases = []ParamAliasEntry{
Blocked: []string{"category-ids"},
},
{
CLIPath: "chat category remove-conv",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"category-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
CLIPath: "chat category remove-conv",
Blocked: []string{"category-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat", "chat-id", "open-conversation-id"},
},
{
CLIPath: "chat category rename",
@@ -970,23 +962,19 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat conversation-info",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
"staff-id": "user",
"uid": "user",
"userid": "user",
"staff-id": "user",
"uid": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat group audit-join-validation",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"staff-id", "uid", "user", "user-id", "userid"},
},
{
@@ -1006,12 +994,9 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat group dismiss",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group get-by-group-id",
@@ -1020,12 +1005,9 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat group invite-url",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group members",
@@ -1100,52 +1082,37 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat group notice create",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group notice edit",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group notice get",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group notice list",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group quit",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group rename",
@@ -1161,23 +1128,17 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat group set-admin",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"user-ids": "users",
"chat-id": "conversation-id",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "userid"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "userid"},
},
{
CLIPath: "chat group set-history",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group share-invite",
@@ -1191,151 +1152,111 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat group transfer-owner",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"staff-id": "user",
"uid": "user",
"userid": "user",
"chat-id": "conversation-id",
"staff-id": "user",
"uid": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
},
{
CLIPath: "chat group update-alias",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-icon",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-nick",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-settings",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group-mute",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-icon",
Aliases: map[string]string{
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-nick",
Aliases: map[string]string{
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group update-settings",
Aliases: map[string]string{
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group-mute",
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat group-mute-member",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
"user-ids": "users",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "userid"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "userid"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat group-role add",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group-role list",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group-role query-user",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"staff-id": "user",
"uid": "user",
"userid": "user",
"chat-id": "conversation-id",
"staff-id": "user",
"uid": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
},
{
CLIPath: "chat group-role remove",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "role-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "role-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat group-role remove-user",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"staff-id": "user",
"uid": "user",
"userid": "user",
"chat-id": "conversation-id",
"staff-id": "user",
"uid": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "role-id", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "role-id", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
},
{
CLIPath: "chat group-role set-user",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"staff-id": "user",
"uid": "user",
"userid": "user",
"chat-id": "conversation-id",
"staff-id": "user",
"uid": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "role-id", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "role-id", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
},
{
CLIPath: "chat group-role update",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "open-conversation-ids", "role-ids", "source", "src-conversation-id", "target"},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "open-conversation-ids", "role-ids", "source", "src-conversation-id", "target"},
},
{
CLIPath: "chat hide",
@@ -1369,12 +1290,10 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message add-emoji",
Aliases: map[string]string{
"chat-id": "conversation-id",
"message-id": "msg-id",
"open-conversation-id": "conversation-id",
"open-message-id": "msg-id",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Ambiguous: []string{"open-message-id"},
},
{
CLIPath: "chat message add-favorite",
@@ -1391,12 +1310,10 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message add-text-emotion",
Aliases: map[string]string{
"chat-id": "conversation-id",
"message-id": "msg-id",
"open-conversation-id": "conversation-id",
"open-message-id": "msg-id",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Ambiguous: []string{"open-message-id"},
},
{
CLIPath: "chat message combine-forward",
@@ -1427,8 +1344,6 @@ var generatedParamAliases = []ParamAliasEntry{
Aliases: map[string]string{
"dest-open-cid": "dest-conversation-id",
"destination-conversation-id": "dest-conversation-id",
"message-id": "msg-id",
"open-message-id": "msg-id",
"source-conversation-id": "src-conversation-id",
"src-open-cid": "src-conversation-id",
"target-conversation-id": "dest-conversation-id",
@@ -1453,21 +1368,20 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message list",
Aliases: map[string]string{
"chat-id": "group",
"max-result": "limit",
"max-results": "limit",
"open-conversation-id": "group",
"page-size": "limit",
"per-page": "limit",
"staff-id": "user",
"start": "time",
"take": "limit",
"top": "limit",
"uid": "user",
"user-id": "user",
"userid": "user",
"max-result": "limit",
"max-results": "limit",
"page-size": "limit",
"per-page": "limit",
"staff-id": "user",
"start": "time",
"take": "limit",
"top": "limit",
"uid": "user",
"user-id": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "count", "cursor", "dest-conversation-id", "end", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "page", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "count", "cursor", "dest-conversation-id", "end", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "page", "source", "src-conversation-id", "target", "to-user", "user-ids", "users"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message list-all",
@@ -1514,12 +1428,9 @@ var generatedParamAliases = []ParamAliasEntry{
},
},
{
CLIPath: "chat message list-mentions",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
CLIPath: "chat message list-mentions",
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message list-pin-msg",
@@ -1534,8 +1445,7 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message list-topic-replies",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
},
@@ -1549,11 +1459,10 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message read-status",
Aliases: map[string]string{
"chat-id": "conversation-id",
"msg-id": "message-id",
"open-conversation-id": "conversation-id",
"open-message-id": "message-id",
"user-ids": "users",
"chat-id": "conversation-id",
"msg-id": "message-id",
"open-message-id": "message-id",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "staff-id", "target", "topic-id", "uid", "userid"},
},
@@ -1561,30 +1470,25 @@ var generatedParamAliases = []ParamAliasEntry{
CLIPath: "chat message recall",
Aliases: map[string]string{
"chat-id": "conversation-id",
"message-id": "msg-id",
"open-conversation-id": "conversation-id",
"open-message-id": "msg-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "target", "topic-id"},
},
{
CLIPath: "chat message recall-by-bot",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
"robot": "robot-code",
"robot": "robot-code",
},
Blocked: []string{"bot-code", "bot-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-bot-id", "open-conversation-ids", "robot-id", "source", "src-conversation-id", "target"},
Blocked: []string{"bot-code", "bot-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-bot-id", "open-conversation-ids", "robot-id", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message remove-emoji",
Aliases: map[string]string{
"chat-id": "conversation-id",
"message-id": "msg-id",
"open-conversation-id": "conversation-id",
"open-message-id": "msg-id",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Ambiguous: []string{"open-message-id"},
},
{
CLIPath: "chat message remove-favorite",
@@ -1601,12 +1505,10 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message remove-text-emotion",
Aliases: map[string]string{
"chat-id": "conversation-id",
"message-id": "msg-id",
"open-conversation-id": "conversation-id",
"open-message-id": "msg-id",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "message-ids", "msg-ids", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "src-msg-id", "topic-id"},
Ambiguous: []string{"open-message-id"},
},
{
CLIPath: "chat message reply",
@@ -1620,12 +1522,9 @@ var generatedParamAliases = []ParamAliasEntry{
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "message-id", "msg-id", "msg-ids", "name", "open-conversation-ids", "open-message-id", "source", "src-conversation-id", "src-msg-id", "staff-id", "target", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "chat message search",
Aliases: map[string]string{
"chat-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
CLIPath: "chat message search",
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message search-advanced",
@@ -1638,28 +1537,25 @@ var generatedParamAliases = []ParamAliasEntry{
{
CLIPath: "chat message send",
Aliases: map[string]string{
"chat-id": "group",
"file": "file-path",
"open-conversation-id": "group",
"staff-id": "user",
"to-user": "user",
"uid": "user",
"user-id": "user",
"userid": "user",
"file": "file-path",
"staff-id": "user",
"to-user": "user",
"uid": "user",
"user-id": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "user-ids", "users"},
Blocked: []string{"at-user-ids", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-conversation-ids", "source", "src-conversation-id", "target", "user-ids", "users"},
Ambiguous: []string{"chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message send-by-bot",
Aliases: map[string]string{
"at-users": "at-user-ids",
"chat-id": "group",
"open-conversation-id": "group",
"robot": "robot-code",
"user-ids": "users",
"at-users": "at-user-ids",
"robot": "robot-code",
"user-ids": "users",
},
Blocked: []string{"bot-code", "bot-id", "conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "name", "open-bot-id", "open-conversation-ids", "robot-id", "source", "src-conversation-id", "staff-id", "target", "to-user-id", "uid", "user", "user-id", "userid"},
Ambiguous: []string{"at-ids"},
Ambiguous: []string{"at-ids", "chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message send-by-webhook",
@@ -1668,14 +1564,9 @@ var generatedParamAliases = []ParamAliasEntry{
},
},
{
CLIPath: "chat message send-card",
Aliases: map[string]string{
"chat": "group",
"chat-id": "group",
"conversation-id": "group",
"open-conversation-id": "group",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "user", "user-id", "userid"},
CLIPath: "chat message send-card",
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "name", "open-conversation-ids", "source", "src-conversation-id", "staff-id", "target", "uid", "user", "user-id", "userid"},
Ambiguous: []string{"chat", "chat-id", "open-conversation-id"},
},
{
CLIPath: "chat message set-pin-msg",
@@ -1684,8 +1575,6 @@ var generatedParamAliases = []ParamAliasEntry{
"chat-id": "open-conversation-id",
"conversation-id": "open-conversation-id",
"group": "open-conversation-id",
"message-id": "msg-id",
"open-message-id": "msg-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "target", "topic-id"},
},
@@ -1696,8 +1585,6 @@ var generatedParamAliases = []ParamAliasEntry{
"chat-id": "open-conversation-id",
"conversation-id": "open-conversation-id",
"group": "open-conversation-id",
"message-id": "msg-id",
"open-message-id": "msg-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "target", "topic-id"},
},
@@ -1708,8 +1595,6 @@ var generatedParamAliases = []ParamAliasEntry{
"chat-id": "open-conversation-id",
"conversation-id": "open-conversation-id",
"group": "open-conversation-id",
"message-id": "msg-id",
"open-message-id": "msg-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "target", "topic-id"},
},
@@ -1720,17 +1605,13 @@ var generatedParamAliases = []ParamAliasEntry{
"chat-id": "open-conversation-id",
"conversation-id": "open-conversation-id",
"group": "open-conversation-id",
"message-id": "msg-id",
"open-message-id": "msg-id",
},
Blocked: []string{"conversation-ids", "dest-conversation-id", "group-id", "group-ids", "group-name", "id", "message-ids", "msg-ids", "name", "open-conversation-ids", "open-message-ids", "open-task-id", "ref-msg-id", "resource-id", "source", "src-conversation-id", "src-msg-id", "target", "topic-id"},
},
{
CLIPath: "chat mute",
Aliases: map[string]string{
"chat-id": "conversation-id",
"group": "conversation-id",
"open-conversation-id": "conversation-id",
"chat-id": "conversation-id",
},
Blocked: []string{"conversation-ids", "group-id", "group-ids", "open-conversation-ids"},
},
@@ -2548,7 +2429,7 @@ var generatedParamAliases = []ParamAliasEntry{
"node-id": "node",
"url": "node",
},
Blocked: []string{"block-id", "comment-id", "comment-key", "dentry-id", "folder", "folder-id", "id", "job-id", "name", "parent-id", "revision", "role", "space-id", "task-id", "template-id", "version", "workspace", "workspace-id"},
Blocked: []string{"block-id", "comment-id", "comment-key", "count", "dentry-id", "folder", "folder-id", "id", "job-id", "name", "offset", "page", "parent-id", "revision", "role", "space-id", "task-id", "template-id", "version", "workspace", "workspace-id"},
Ambiguous: []string{"max-result", "max-results", "next-cursor", "next-page-token", "next-token", "per-page", "size", "take", "top"},
},
{
+104
View File
@@ -237,6 +237,42 @@ func TestReduceLeafParamAliasesRemainingEdges(t *testing.T) {
})
}
func TestCrossPlatformCoverageReduceLeafParamAliasesBindExcludesRealFlags(t *testing.T) {
entry, problems := reduceLeafParamAliases(
"demo cmd",
realMap(realFlag{name: "id"}, realFlag{name: "name"}, realFlag{name: "query"}),
[]Concept{
{ID: "base_id", Members: []string{"base-id", "base-token"}, Excludes: []string{"keyword", "name", "query", "unsafe"}},
{ID: "query", Members: []string{"query", "keyword"}},
},
CommandOverride{Bind: map[string]string{"id": "base_id"}},
)
if len(problems) != 0 {
t.Fatalf("reduceLeafParamAliases() problems = %v", problems)
}
if entry == nil {
t.Fatal("reduceLeafParamAliases() entry = nil")
}
if entry.Aliases["base-id"] != "id" || entry.Aliases["base-token"] != "id" {
t.Fatalf("bound aliases = %#v, want base-id/base-token -> id", entry.Aliases)
}
if entry.Aliases["keyword"] != "query" {
t.Fatalf("query alias = %#v, want keyword -> query", entry.Aliases)
}
if containsParamAlias(entry.Blocked, "keyword") {
t.Fatalf("excluded alias entered blocked list: %#v", entry.Blocked)
}
if containsParamAlias(entry.Blocked, "name") {
t.Fatalf("real excluded flag entered blocked list: %#v", entry.Blocked)
}
if containsParamAlias(entry.Blocked, "query") {
t.Fatalf("claimed real excluded flag entered blocked list: %#v", entry.Blocked)
}
if !containsParamAlias(entry.Blocked, "unsafe") {
t.Fatalf("non-real excluded flag was not blocked: %#v", entry.Blocked)
}
}
func TestParamAliasEntryLookupMethods(t *testing.T) {
entry := ParamAliasEntry{
Aliases: map[string]string{"uid": "user"},
@@ -457,6 +493,22 @@ func TestReduceLeafParamAliasesExcludesProtectFuzzyButDoNotOverrideAnotherConcep
}
}
func TestReduceLeafParamAliasesExcludesDoNotBlockRealFlag(t *testing.T) {
concepts := []Concept{
{ID: "single_id", Members: []string{"id", "item-id"}, Excludes: []string{"item-ids"}},
}
entry, problems := reduceLeafParamAliases("demo cmd", realMap(realFlag{name: "id"}, realFlag{name: "item-ids"}), concepts, CommandOverride{})
if len(problems) != 0 {
t.Fatalf("unexpected problems: %v", problems)
}
if entry == nil {
t.Fatal("expected a reduced entry")
}
if containsParamAlias(entry.Blocked, "item-ids") {
t.Fatalf("real exclude was blocked: %#v", entry)
}
}
func TestReduceLeafParamAliasesRejectsProtectionOrScopedAliasOnRealFlag(t *testing.T) {
real := realMap(realFlag{name: "user-id"}, realFlag{name: "user"})
for name, override := range map[string]CommandOverride{
@@ -472,6 +524,58 @@ func TestReduceLeafParamAliasesRejectsProtectionOrScopedAliasOnRealFlag(t *testi
}
}
func TestGeneratedParamAliasesBlockPluralListSpellingsOnSingleIDCommands(t *testing.T) {
entries := make(map[string]ParamAliasEntry, len(generatedParamAliases))
for _, entry := range generatedParamAliases {
entries[entry.CLIPath] = entry
}
assertBlocked := func(path string, names ...string) {
t.Helper()
entry, ok := entries[path]
if !ok {
t.Fatalf("missing generated alias entry for %q", path)
}
for _, name := range names {
if !entry.IsBlocked(cmdutil.Morph(name)) {
t.Fatalf("%s: %q not blocked; entry = %#v", path, name, entry)
}
}
}
for _, path := range []string{
"chat message add-emoji",
"chat message remove-emoji",
"chat message add-text-emotion",
"chat message remove-text-emotion",
} {
assertBlocked(path, "msg-ids", "message-ids")
}
for _, path := range []string{
"chat message send",
"chat conversation-info",
"chat category add-conv",
"chat category remove-conv",
"chat message list",
"chat message list-mentions",
"chat message recall-by-bot",
"chat message search",
} {
assertBlocked(path, "conversation-ids")
}
}
func TestGeneratedParamAliasesKeepAuditJoinUserRoleAmbiguous(t *testing.T) {
entry, ok := LookupParamAlias("chat group audit-join-validation")
if !ok {
t.Fatal("missing generated alias entry for chat group audit-join-validation")
}
for _, name := range []string{"user", "user-id", "userid", "uid", "staff-id"} {
if !entry.IsAmbiguous(cmdutil.Morph(name)) {
t.Fatalf("%q not ambiguous; entry = %#v", name, entry)
}
}
}
// TestGeneratedParamAliasesAreWellFormed guards the committed generated table
// at the Go level, complementing the byte-identity drift gate.
func TestGeneratedParamAliasesAreWellFormed(t *testing.T) {
+19 -12
View File
@@ -60,11 +60,17 @@
"chat group members": {"bind": {"id": "open_conversation_id"}},
"chat group members add": {"bind": {"id": "open_conversation_id"}, "block": ["user-id", "open-dingtalk-id"], "note": "The real --users is a list and may contain mixed userId/openDingTalkId values; singular inputs are not promoted automatically."},
"chat group members remove": {"bind": {"id": "open_conversation_id"}},
"chat message add-emoji": {"scoped_aliases": {"chat-id": "conversation-id", "open-conversation-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --chat/--group/--id/--conversation-id stay native; numeric groupId and list spellings are rejected."},
"chat message add-text-emotion": {"scoped_aliases": {"chat-id": "conversation-id", "open-conversation-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --chat/--group/--id/--conversation-id stay native; numeric groupId and list spellings are rejected."},
"chat message remove-emoji": {"scoped_aliases": {"chat-id": "conversation-id", "open-conversation-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --chat/--group/--id/--conversation-id stay native; numeric groupId and list spellings are rejected."},
"chat message remove-text-emotion": {"scoped_aliases": {"chat-id": "conversation-id", "open-conversation-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --chat/--group/--id/--conversation-id stay native; numeric groupId and list spellings are rejected."},
"chat mute": {"scoped_aliases": {"group": "conversation-id", "chat-id": "conversation-id", "open-conversation-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --conversation-id/--id/--chat remain unchanged; other reviewed openConversationId spellings reduce to --conversation-id."},
"chat conversation-info": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat group-mute": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat group-mute-member": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat message add-emoji": {"scoped_aliases": {"chat-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "ambiguous": ["open-message-id"], "note": "Native --chat/--group/--id/--conversation-id/--open-conversation-id and visible --message-id/--msg-id stay executable; numeric/list spellings are rejected."},
"chat message add-text-emotion": {"scoped_aliases": {"chat-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "ambiguous": ["open-message-id"], "note": "Native --chat/--group/--id/--conversation-id/--open-conversation-id and visible --message-id/--msg-id stay executable; numeric/list spellings are rejected."},
"chat message list-mentions": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat message recall-by-bot": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat message remove-emoji": {"scoped_aliases": {"chat-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "ambiguous": ["open-message-id"], "note": "Native --chat/--group/--id/--conversation-id/--open-conversation-id and visible --message-id/--msg-id stay executable; numeric/list spellings are rejected."},
"chat message remove-text-emotion": {"scoped_aliases": {"chat-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "ambiguous": ["open-message-id"], "note": "Native --chat/--group/--id/--conversation-id/--open-conversation-id and visible --message-id/--msg-id stay executable; numeric/list spellings are rejected."},
"chat message search": {"ambiguous": ["chat-id", "open-conversation-id"], "note": "Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat mute": {"scoped_aliases": {"chat-id": "conversation-id"}, "block": ["group-id", "group-ids", "conversation-ids", "open-conversation-ids"], "note": "Native --conversation-id and hidden compatibility --group/--id/--chat/--open-conversation-id remain unchanged; reviewed chat-id reduces to --conversation-id."},
"drive list": {"ambiguous": ["root-id", "space"], "note": "Numeric --space-id and knowledge-base --workspace are distinct routes; bare --space/--root-id cannot select a domain or folder.", "scoped_aliases": {"document-id": "node", "dentry-uuid": "node", "directory-id": "folder", "order-field": "order-by", "sort-by": "order-by", "sort-field": "order-by"}, "scope_strict": true, "block": ["dentry-id"]},
"drive upload": {"ambiguous": ["destination-id", "space", "target-id"], "note": "Local file, display name, MIME, overwrite node, folder, storage space, and knowledge-base workspace remain distinct roles.", "scoped_aliases": {"dentry-uuid": "node", "directory-id": "folder", "overwrite-node-id": "node", "target-folder-id": "folder", "target-workspace-id": "workspace", "source-file": "file", "content-type": "mime-type", "filename": "file-name", "name": "file-name", "display-name": "file-name", "upload-name": "file-name"}, "block": ["dentry-id", "document-url", "output-path"], "scope_strict": true},
"ding +receiver-status": {"scoped_aliases": {"id": "ding-id"}, "note": "generic id reduces to ding-id"},
@@ -84,12 +90,12 @@
"chat +unread-chats": {"scoped_aliases": {"limit": "count", "size": "count"}, "scope_strict": true, "note": "On this exact command, limit and size both denote the returned unread-conversation count."},
"chat message list-unread-conversations": {"scoped_aliases": {"limit": "count", "size": "count"}, "scope_strict": true, "note": "On this exact command, limit and size both denote the returned unread-conversation count."},
"chat +messages-list-direct": {"scoped_aliases": {"start": "time"}, "block": ["end"], "scope_strict": true, "note": "This exact command accepts one start boundary in yyyy-MM-dd HH:mm:ss; an end-only input cannot be represented."},
"chat message list": {"scoped_aliases": {"start": "time"}, "block": ["end"], "scope_strict": true, "note": "This exact command accepts one start boundary in yyyy-MM-dd HH:mm:ss; an end-only input cannot be represented."},
"chat message list": {"scoped_aliases": {"start": "time"}, "block": ["end"], "ambiguous": ["chat-id", "open-conversation-id"], "scope_strict": true, "note": "This exact command accepts one start boundary in yyyy-MM-dd HH:mm:ss; an end-only input cannot be represented. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat message list-by-sender": {"scoped_aliases": {"user-id": "sender-user-id", "open-dingtalk-id": "sender-open-dingtalk-id"}, "block": ["time"], "scope_strict": true, "note": "Only same-role sender identifiers are mapped; --time cannot supply the required RFC3339 start/end range."},
"contact +resolve-dept": {"bind": {"name": "search_query"}, "note": "The real --name is a department-name search keyword and carries the search_query concept on this shortcut."},
"contact +list-sub-depts": {"block": ["name", "query"], "note": "--dept is an integer department id; names and search queries require a separate resolution command"},
"contact +dept-members": {"bind": {"dept": "search_query"}, "scoped_aliases": {"name": "dept"}, "note": "The real --dept is a department-name search keyword; search spellings come from search_query, while --name remains command-scoped."},
"chat message send": {"scoped_aliases": {"to-user": "user", "file": "file-path"}, "note": "Recipient and local-file-path aliases are exact to this command; obsolete file metadata flags remain unsupported."},
"chat message send": {"scoped_aliases": {"to-user": "user", "file": "file-path"}, "ambiguous": ["chat-id", "open-conversation-id"], "note": "Recipient and local-file-path aliases are exact to this command; obsolete file metadata flags remain unsupported. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat +group-members": {"bind": {"group": "group_name"}, "note": "The real --group is a group-name search keyword on this shortcut, not an identifier."},
"chat +category-create": {"scoped_aliases": {"name": "title"}, "scope_strict": true, "note": "The reviewed name/title mapping preserves the category display-name value on this exact shortcut."},
"chat category create": {"scoped_aliases": {"name": "title"}, "scope_strict": true, "note": "The reviewed name/title mapping preserves the category display-name value on this exact command."},
@@ -98,8 +104,8 @@
"chat +category-delete": {"block": ["category-ids"], "note": "This command requires one category id; list cardinality is not reduced automatically."},
"chat category delete": {"block": ["category-ids"], "note": "This command requires one category id; list cardinality is not reduced automatically."},
"chat category list-conversations": {"block": ["category-ids"], "note": "This command requires one category id; list cardinality is not reduced automatically."},
"chat category add-conv": {"block": ["category-id"], "note": "This command requires a category-id list; one id is not promoted into a batch input."},
"chat category remove-conv": {"block": ["category-id"], "note": "This command requires a category-id list; one id is not promoted into a batch input."},
"chat category add-conv": {"block": ["category-id"], "ambiguous": ["chat", "chat-id", "open-conversation-id"], "note": "This command requires a category-id list; one id is not promoted into a batch input. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat category remove-conv": {"block": ["category-id"], "ambiguous": ["chat", "chat-id", "open-conversation-id"], "note": "This command requires a category-id list; one id is not promoted into a batch input. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat +chat-role-update": {"block": ["role-ids"], "note": "This command requires one role id; list cardinality is not reduced automatically."},
"chat group-role remove": {"block": ["role-ids"], "note": "This command requires one role id; list cardinality is not reduced automatically."},
"chat group-role update": {"block": ["role-ids"], "note": "This command requires one role id; list cardinality is not reduced automatically."},
@@ -109,7 +115,7 @@
"chat +messages-send-by-webhook": {"scoped_aliases": {"at-user-ids": "at-users"}, "scope_strict": true, "note": "Both names denote the same userId list used for @ mentions on this exact shortcut."},
"chat message send-by-webhook": {"scoped_aliases": {"at-user-ids": "at-users"}, "scope_strict": true, "note": "Both names denote the same userId list used for @ mentions on this exact command."},
"doc block insert": {"block": ["before-block-id"], "note": "Parent and reference roles remain distinct. --before-block-id needs both --ref-block and --where before, while role-free --block-id cannot choose parent versus reference.", "scoped_aliases": {"parent-block-id": "parent-block", "ref-block-id": "ref-block", "reference-block-id": "ref-block"}, "ambiguous": ["block-id"], "scope_strict": true},
"chat message send-by-bot": {"scoped_aliases": {"at-users": "at-user-ids"}, "block": ["user-id", "to-user-id"], "ambiguous": ["at-ids"], "note": "The reviewed @ userId-list alias is exact; singular recipients are not promoted, and bare --at-ids cannot choose an identifier domain."},
"chat message send-by-bot": {"scoped_aliases": {"at-users": "at-user-ids"}, "block": ["user-id", "to-user-id"], "ambiguous": ["at-ids", "chat-id", "open-conversation-id"], "note": "The reviewed @ userId-list alias is exact; singular recipients are not promoted, and bare --at-ids cannot choose an identifier domain. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"doc +export-get": {"block": ["doc-id", "document-id", "file-id", "node", "node-id", "task-id", "url"], "note": "This command queries one export jobId. Document node identifiers and import taskId spellings are different entities and are rejected.", "scoped_aliases": {"export-job-id": "job-id"}, "scope_strict": true},
"doc block delete": {"block": ["index"], "note": "index (position) vs node (node id) are different"},
"doc +copy": {"scoped_aliases": {"folder-id": "folder", "parent-folder": "folder", "parent-folder-id": "folder", "parent-node-id": "folder", "space": "workspace", "space-id": "workspace"}, "block": ["parent-id"], "scope_strict": true, "note": "This exact Doc command expects a Doc folder nodeId/dentryUuid/URL. Reviewed folder spellings preserve that value; generic --parent-id stays blocked because it may carry a numeric Drive dentryId. The command-scoped --space/--space-id aliases preserve the compatibility published before workspace and numeric DingDrive storage-space concepts were split; they do not make those value domains globally equivalent."},
@@ -142,7 +148,7 @@
"chat category create-smart": {"bind": {"members": "open_dingtalk_ids"}, "scoped_aliases": {"title": "name"}, "note": "The real --members is an openDingTalkId list; the reviewed title/name alias is exact to the category display name."},
"chat group audit-join-validation": {"ambiguous": ["user", "user-id", "userid", "uid", "staff-id"], "note": "A role-free user identifier cannot choose between the required --applicant and --inviter roles."},
"chat message reply": {"block": ["user", "user-id", "userid", "uid", "staff-id"], "note": "The required --ref-sender is a role-specific openDingTalkId and must not accept generic userId spellings."},
"chat message send-card": {"block": ["user", "user-id", "userid", "uid", "staff-id"], "note": "The real --receiver is a role-specific openDingTalkId and must not accept generic userId spellings."},
"chat message send-card": {"block": ["user", "user-id", "userid", "uid", "staff-id"], "ambiguous": ["chat", "chat-id", "open-conversation-id"], "note": "The real --receiver is a role-specific openDingTalkId and must not accept generic userId spellings. Visible --conversation-id and legacy --group both remain executable outside the approved hidden-alias migration set."},
"chat +category-add-conversation": {"block": ["category-id"], "note": "The real --category-ids is a list; a singular category ID is not promoted automatically."},
"chat +category-list-conversations": {"block": ["category-ids"], "note": "The real --category-id is singular; list cardinality is not reduced automatically."},
"chat +category-remove-conversation": {"block": ["category-id"], "note": "The real --category-ids is a list; a singular category ID is not promoted automatically."},
@@ -328,7 +334,7 @@
{"command": "chat group rename", "emitted": "conversation-id", "expect": "id", "via": "concept:open_conversation_id+bind"},
{"command": "chat group rename", "emitted": "group-id", "expect": "did-you-mean:blocked", "via": "guard:group-id-vs-open-conversation-id"},
{"command": "chat message send", "emitted": "conversation-id", "expect": "group", "via": "concept:open_conversation_id"},
{"command": "chat message add-emoji", "emitted": "open-conversation-id", "expect": "conversation-id", "via": "override:scoped"},
{"command": "chat message add-emoji", "emitted": "open-conversation-id", "expect": "conversation-id", "via": "concept:open_conversation_id"},
{"command": "chat message add-emoji", "emitted": "group-id", "expect": "did-you-mean:blocked", "via": "guard:group-id-vs-open-conversation-id"},
{"command": "chat +group-members", "emitted": "conversation-id", "expect": "did-you-mean:blocked", "via": "guard:group-name-vs-open-conversation-id"},
{"command": "chat +send-to-group", "emitted": "group-name", "expect": "group", "via": "concept:group_name+bind"},
@@ -466,6 +472,7 @@
{"command": "chat +flag-create", "emitted": "group", "expect": "conversation-id", "via": "concept:open_conversation_id"},
{"command": "chat +chat-add-bot", "emitted": "conversation-id", "expect": "id", "via": "concept:open_conversation_id+bind"},
{"command": "chat +chat-add-bot", "emitted": "robot", "expect": "robot-code", "via": "concept:robot_code"},
{"command": "chat group audit-join-validation", "emitted": "user", "expect": "did-you-mean:ambiguous", "via": "guard:applicant-vs-inviter-role"},
{"command": "chat +chat-audit-join", "emitted": "applicant-user-id", "expect": "applicant", "via": "override:scoped-user-role"},
{"command": "chat +chat-audit-join", "emitted": "user-id", "expect": "did-you-mean:ambiguous", "via": "guard:applicant-vs-inviter-role"},
{"command": "chat +chat-create", "emitted": "user-id", "expect": "did-you-mean:blocked", "via": "guard:single-vs-list"},
+52 -8
View File
@@ -1354,16 +1354,16 @@ func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) {
interfaceType string
}{
{"chat message edit", "conversation-id", "openConversationId", true, ""},
{"chat message edit", "msg-id", "openMessageId", true, ""},
{"chat message edit", "message-id", "openMessageId", true, ""},
{"chat message edit", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"},
{"chat message update-text-emotion", "message-id", "openMsgId", true, ""},
{"chat message send", "idempotency-key", "uuid", false, ""},
{"chat message send-card", "at-all", "atAll", false, ""},
{"chat message send-card", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"},
{"chat message update-text-emotion", "msg-id", "openMsgId", true, ""},
{"chat message update-text-emotion", "old-emotion-id", "oldEmotionId", true, ""},
{"chat category batch-info", "category-ids", "categoryIds", true, "array"},
{"chat category list-by-conv", "group", "openConversationId", true, ""},
{"chat group update-nick", "group", "openConversationId", true, ""},
{"chat category list-by-conv", "conversation-id", "openConversationId", true, ""},
{"chat group update-nick", "conversation-id", "", true, ""},
{"chat group upgrade-to-external", "extension", "extension", false, "object"},
{"chat +messages-send-card", "receiver-open-dingtalk-id", "receiverOpenDingTalkId", false, ""},
{"chat message list-favorites", "size", "", false, "string"},
@@ -1397,7 +1397,8 @@ func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) {
}
}
// Hidden conversation aliases must stay unpublished (merge-base parity).
// Manifest-covered migrations hide legacy aliases; manifest-external
// commands keep their existing visible flags for compatibility.
editLeaf, err := queryDeliverySchemaPayload([]string{"chat message edit"})
if err != nil {
t.Fatal(err)
@@ -1421,9 +1422,52 @@ func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) {
t.Fatal(err)
}
listParams := schemaMap(listByConv["parameters"])
for _, hidden := range []string{"conversation-id", "id"} {
if _, ok := listParams[hidden]; ok {
t.Fatalf("chat category list-by-conv unexpectedly publishes hidden alias --%s", hidden)
if _, ok := listParams["conversation-id"]; !ok {
t.Fatalf("chat category list-by-conv missing public canonical --conversation-id")
}
if _, ok := listParams["group"]; !ok {
t.Fatalf("chat category list-by-conv unexpectedly hides manifest-external --group")
}
if _, ok := listParams["id"]; ok {
t.Fatalf("chat category list-by-conv unexpectedly publishes hidden alias --id")
}
for _, path := range []string{
"chat message add-emoji",
"chat message remove-emoji",
"chat message add-text-emotion",
"chat message remove-text-emotion",
} {
leaf, err := queryDeliverySchemaPayload([]string{path})
if err != nil {
t.Fatal(err)
}
params := schemaMap(leaf["parameters"])
if _, ok := params["conversation-id"]; !ok {
t.Fatalf("%s missing public canonical --conversation-id", path)
}
for _, visible := range []string{"group", "id", "chat"} {
if _, ok := params[visible]; !ok {
t.Fatalf("%s unexpectedly hides manifest-external --%s", path, visible)
}
}
}
groupBots, err := queryDeliverySchemaPayload([]string{"chat group bots"})
if err != nil {
t.Fatal(err)
}
groupBotsParams := schemaMap(groupBots["parameters"])
group := groupBotsParams["group"]
if group == nil {
t.Fatal("chat group bots missing public legacy --group")
}
if group["property"] != "openConversationId" {
t.Fatalf("chat group bots --group property = %#v, want openConversationId", group["property"])
}
for _, migrated := range []string{"conversation-id", "group-name"} {
if _, ok := groupBotsParams[migrated]; ok {
t.Fatalf("chat group bots unexpectedly publishes migrated --%s", migrated)
}
}
}
@@ -43,19 +43,14 @@ func init() {
RequireOneOf: [][]string{{"conversation-id", "open-dingtalk-id", "user", "permParam"}},
})
registerExclusiveOneOf("chat.chat_permission_grant_cross_org_data", "target-org-id", "all")
registerRequireOneOf("chat.add_emoji_reaction", "conversation-id", "group", "id", "chat")
registerRequireOneOf("chat.add_text_emotion", "conversation-id", "group", "id", "chat")
registerExclusiveOneOf("chat.clear_conversation_messages", "conversation-id", "id", "chat")
registerExclusiveOneOf("chat.clear_conversation_red_point", "conversation-id", "id", "chat")
registerRequireOneOf("chat.update_text_emotion", "conversation-id", "group", "id", "chat")
registerExclusiveOneOf("chat.get_conversation_info", "group", "user", "open-dingtalk-id")
registerExclusiveOneOf("chat.hide_conversation", "conversation-id", "id", "chat")
registerExclusiveOneOf("chat.list_conversation_message_v2", "group", "user", "open-dingtalk-id")
registerExclusiveOneOf("chat.list_individual_chat_message", "user", "open-dingtalk-id")
registerExclusiveOneOf("chat.mark_conversation_unread", "conversation-id", "id", "chat")
registerExclusiveOneOf("chat.mark_message_read", "conversation-id", "id", "chat")
registerRequireOneOf("chat.remove_emoji_reaction", "conversation-id", "group", "id", "chat")
registerRequireOneOf("chat.remove_text_emotion", "conversation-id", "group", "id", "chat")
registerRequireOneOf("chat.send_personal_message", "text", "content", "msg-type")
registerExclusiveOneOf("chat.send_robot_message", "group", "users")
registerRequireOneOf("chat.set_group_member_mute_list", "users", "user")
@@ -252,7 +252,8 @@ var reviewedSchemaParameterMappingExclusions = map[string]string{
"chat.batch_query_group_chat_settings --groups": "Reviewed unpinned adapter: chat.batch_query_group_chat_settings has no singular pinned interface_ref; --groups is a CLI wrapper input and does not publish a direct interface property.",
"chat.batch_update_group_chat_settings --items": "Reviewed unpinned adapter: chat.batch_update_group_chat_settings has no singular pinned interface_ref; --items is a CLI wrapper input and does not publish a direct interface property.",
"chat.create_text_emotion --background-id": "Runtime extension: the executable helper forwards backgroundId to im/create_text_emotion, but the pinned source-revision metadata does not declare that optional property; preserve the compatibility flag without advertising it as a pinned RPC field.",
"chat.get_group_mute_config --group": "Reviewed unpinned adapter: chat.get_group_mute_config has no singular pinned interface_ref; --group is a CLI wrapper input and does not publish a direct interface property.",
"chat.get_group_mute_config --conversation-id": "Reviewed unpinned adapter: chat.get_group_mute_config has no singular pinned interface_ref; --conversation-id is a CLI wrapper input and does not publish a direct interface property.",
"chat.get_group_mute_config --group": "Reviewed legacy Schema compatibility: the historical visible --group wrapper input was required but did not publish a direct interface property.",
"chat.list_conversation_message_v2 --open-dingtalk-id": "selects the alternate list_individual_chat_message branch",
"chat.list_conversation_message_v2 --user": "selects the alternate list_individual_chat_message branch",
"chat.list_message_favorites --cursor": "Reviewed unpinned adapter: chat.list_message_favorites has no singular pinned interface_ref; --cursor is a CLI wrapper input and does not publish a direct interface property.",
@@ -265,6 +266,7 @@ var reviewedSchemaParameterMappingExclusions = map[string]string{
"chat.query_msg_read_status --users": "conditional wrapper/alias of --user: parseCSVValues + appendChatIDArgs routes each supplied identifier to targetUserIds or targetOpenDingTalkIds according to its runtime ID shape; there is no single RPC property for this flag",
"chat.remove_message_favorite --open-conversation-id": "Reviewed unpinned adapter: chat.remove_message_favorite has no singular pinned interface_ref; --open-conversation-id is a CLI wrapper input and does not publish a direct interface property.",
"chat.remove_message_favorite --open-message-id": "Reviewed unpinned adapter: chat.remove_message_favorite has no singular pinned interface_ref; --open-message-id is a CLI wrapper input and does not publish a direct interface property.",
"chat.update_text_emotion --conversation-id": "Reviewed unpinned adapter: chat.update_text_emotion has no singular pinned interface_ref; --conversation-id is a CLI wrapper input and does not publish a direct interface property.",
"chat.reply_personal_message --ref-msg-id": "serialized into the aggregate content JSON string",
"chat.reply_personal_message --ref-sender": "resolved then serialized into the aggregate content JSON string",
"chat.reply_personal_message --text": "serialized into the aggregate content JSON string",
+25 -12
View File
@@ -578,18 +578,7 @@ func RegisterFlags(cmd *cobra.Command, flags []FlagSpec) {
for _, alias := range flag.Aliases {
RegisterFlag(cmd, flag.Kind, alias, "", flag.Usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
if registered := cmd.Flags().Lookup(alias); registered != nil {
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOf,
flag.Name,
)
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOrigin,
runtimeannotate.FlagAliasOriginCorecmdV1,
)
}
AnnotateFlagAlias(cmd, alias, flag.Name)
}
if flag.MarkRequired {
_ = cmd.MarkFlagRequired(flag.Name)
@@ -600,6 +589,30 @@ func RegisterFlags(cmd *cobra.Command, flags []FlagSpec) {
}
}
// AnnotateFlagAlias records framework-owned evidence that aliasName is a hidden
// compatibility alias for canonicalName. It is for commands that already own
// their Cobra flag registration outside FlagSpec but still need the same
// interface-snapshot alias contract as FlagSpec.Aliases.
func AnnotateFlagAlias(cmd *cobra.Command, aliasName, canonicalName string) {
if cmd == nil {
return
}
registered := cmd.Flags().Lookup(aliasName)
if registered == nil {
return
}
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOf,
canonicalName,
)
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOrigin,
runtimeannotate.FlagAliasOriginCorecmdV1,
)
}
// RegisterFlag registers one flag by Kind. Default is applied at registration
// for every kind so --help DefValue matches the declared fallback.
// Malformed KindInt / KindBool Default values panic at registration (fail-closed)
+10
View File
@@ -116,6 +116,16 @@ func TestCrossPlatformCoverageRegisterFlagsAllKinds(t *testing.T) {
}
}
func TestCrossPlatformCoverageAnnotateFlagAliasIgnoresMissingInputs(t *testing.T) {
AnnotateFlagAlias(nil, "alias", "canonical")
cmd := newTestCommand()
AnnotateFlagAlias(cmd, "missing", "canonical")
if flag := cmd.Flags().Lookup("missing"); flag != nil {
t.Fatalf("unexpected missing flag registered: %#v", flag)
}
}
// ── effective value fallback chain ─────────────────────────────────
func TestCrossPlatformCoverageEffectiveValueFallbackChain(t *testing.T) {
+808 -446
View File
File diff suppressed because it is too large Load Diff
+260 -8
View File
@@ -3,6 +3,7 @@ package helpers
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -11,6 +12,7 @@ import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
func runChatCoverageCommand(t *testing.T, caller edition.ToolCaller, args ...string) error {
@@ -32,14 +34,26 @@ func runChatCoverageCommand(t *testing.T, caller edition.ToolCaller, args ...str
func runChatCoverageDirect(t *testing.T, path []string, flags map[string]string) error {
t.Helper()
command, _, err := newChatCommand().Find(path)
InitDeps(&scriptedToolCaller{})
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
root := newChatCommand()
installExampleGlobalFlags(root)
root.PersistentFlags().Bool("debug", false, "")
root.PersistentFlags().Bool("verbose", false, "")
command, _, err := root.Find(path)
if err != nil {
return err
}
for name, value := range flags {
if err := command.Flags().Set(name, value); err != nil {
flag := command.Flag(name)
if flag == nil {
return fmt.Errorf("no such flag -%s", name)
}
if err := flag.Value.Set(value); err != nil {
return err
}
flag.Changed = true
}
return command.RunE(command, nil)
}
@@ -93,7 +107,7 @@ func TestCrossPlatformCoverageChatStableCompatibilityHintsRemainAvailable(t *tes
hint string
}{
{path: "send", args: []string{"send", "--group", "cid-stable", "--text", "hello"}, hint: "dws chat message send"},
{path: "history", args: []string{"history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"},
{path: "history", args: []string{"history", "--group", "cid-stable", "--limit", "20"}, hint: "dws chat message list --conversation-id <GROUP_OPEN_CONVERSATION_ID>"},
} {
command, remaining, err := root.Find([]string{tc.path})
if err != nil {
@@ -113,6 +127,80 @@ func TestCrossPlatformCoverageChatStableCompatibilityHintsRemainAvailable(t *tes
}
}
func TestCrossPlatformCoverageChatAliasInstallerRemainingEdges(t *testing.T) {
restoreChatManifestExternalVisibleFlags(nil)
mismatchedRoot := &cobra.Command{Use: "chat"}
mismatchedRoot.AddCommand(&cobra.Command{Use: "other"})
restoreChatManifestExternalVisibleFlags(mismatchedRoot)
missingPrimary := &cobra.Command{Use: "leaf"}
installChatFlagAliases(missingPrimary, "conversation-id", []string{"group"}, requireChatConversationID)
if flag := missingPrimary.Flags().Lookup("group"); flag != nil {
t.Fatalf("alias registered without canonical flag: %#v", flag)
}
skipGroup := &cobra.Command{Use: "leaf", RunE: func(cmd *cobra.Command, args []string) error { return nil }}
skipGroup.Flags().String("conversation-id", "", "")
skipGroup.Flags().String("group-name", "", "")
installChatFlagAliases(skipGroup, "conversation-id", []string{"group", "chat"}, requireChatConversationID)
if flag := skipGroup.Flags().Lookup("group"); flag != nil {
t.Fatalf("group alias registered beside group-name: %#v", flag)
}
if flag := skipGroup.Flags().Lookup("chat"); flag == nil {
t.Fatal("non-group alias was not registered")
}
preRunCalled := false
withPreRun := &cobra.Command{
Use: "leaf",
PreRunE: func(cmd *cobra.Command, args []string) error {
preRunCalled = true
return nil
},
RunE: func(cmd *cobra.Command, args []string) error { return nil },
}
withPreRun.Flags().String("conversation-id", "", "")
installChatFlagAliases(withPreRun, "conversation-id", []string{"group"}, requireChatConversationID)
withPreRun.SetArgs([]string{"--group", "cid"})
if err := withPreRun.ExecuteContext(context.Background()); err != nil {
t.Fatalf("execute with alias and previous PreRunE: %v", err)
}
if !preRunCalled {
t.Fatal("previous PreRunE was not called")
}
}
func TestCrossPlatformCoverageChatMessageForwardRequiresMessageID(t *testing.T) {
caller := &productExampleCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
root := newChatCommand()
command, _, err := root.Find([]string{"message", "forward"})
if err != nil {
t.Fatal(err)
}
for _, name := range []string{"message-id", "msg-id"} {
if flag := command.Flags().Lookup(name); flag != nil && flag.Annotations != nil {
delete(flag.Annotations, cobra.BashCompOneRequiredFlag)
}
}
if err := command.Flags().Set("src-conversation-id", "src"); err != nil {
t.Fatal(err)
}
if err := command.Flags().Set("dest-conversation-id", "dest"); err != nil {
t.Fatal(err)
}
err = command.RunE(command, nil)
if err == nil || !strings.Contains(err.Error(), "missing required flag: --message-id") {
t.Fatalf("forward missing message id error = %v", err)
}
if caller.calls != 0 {
t.Fatalf("tool calls = %d, want 0", caller.calls)
}
}
func TestCrossPlatformCoverageChatGroupUpdateIconAcceptsUploadedMediaIDPrefixes(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
@@ -199,9 +287,10 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T)
{"category", "remove-conv", "--group=cid", "--category-ids=1,2"},
{"message", "list-by-ids", "--msg-ids=" + strings.Repeat("id,", 51) + "last"},
{"group", "transfer-owner", "--group=cid", "--new-owner=D-owner"},
{"group", "transfer-owner", "--group=cid", "--new-owner=DAAAAAAAAAAAiE"},
{"group", "update-icon", "--group=cid", "--icon-media-id=@valid"},
{"group", "set-history", "--group=cid", "--option=ALL"},
{"group", "audit-join-validation", "--group=cid", "--record-id=1", "--applicant=D1", "--inviter=D2", "--status=AuditApprove", "--description=ok"},
{"group", "audit-join-validation", "--conversation-id=cid", "--record-id=1", "--applicant=D1", "--inviter=D2", "--status=AuditApprove", "--description=ok"},
{"mark-read", "--conversation-id=cid", "--message-id=mid"},
{"text", "translate", "--query=hello", "--to=zh_CN"},
{"group-role", "set-user", "--group=cid", "--user=D1", "--role-ids=r1"},
@@ -293,7 +382,7 @@ func TestCrossPlatformCoverageChatNativeSendCardMentions(t *testing.T) {
caller := &scriptedToolCaller{}
err := runChatCoverageCommand(t, caller,
"message", "send-card",
"--group=cid",
"--conversation-id=cid",
"--at-open-dingtalk-ids=D1,D2,D1",
"--at-all",
)
@@ -314,13 +403,13 @@ func TestCrossPlatformCoverageChatNativeSendCardMentions(t *testing.T) {
name string
args []string
}{
{name: "member mention rejects direct message", args: []string{"--receiver=D1", "--at-open-dingtalk-ids=D2"}},
{name: "at all rejects direct message", args: []string{"--receiver=D1", "--at-all"}},
{name: "member mention rejects direct message", args: []string{"--open-dingtalk-id=D1", "--at-open-dingtalk-ids=D2"}},
{name: "at all rejects direct message", args: []string{"--open-dingtalk-id=D1", "--at-all"}},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
err := runChatCoverageCommand(t, caller, append([]string{"message", "send-card"}, tc.args...)...)
if err == nil || !strings.Contains(err.Error(), "only supported with --group") {
if err == nil || !strings.Contains(err.Error(), "only supported with --conversation-id") {
t.Fatalf("error = %v, want group-only mention validation", err)
}
if caller.calls != 0 {
@@ -330,6 +419,168 @@ func TestCrossPlatformCoverageChatNativeSendCardMentions(t *testing.T) {
}
}
func TestCrossPlatformCoverageChatSendCardHiddenAliasesMapToCanonicalPayload(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs })
for _, tc := range []struct {
name string
args []string
want map[string]any
}{
{
name: "group alias",
args: []string{"--group=cid"},
want: map[string]any{"openConversationId": "cid"},
},
{
name: "receiver alias",
args: []string{"--receiver=DAAAAAAAAAAAiE"},
want: map[string]any{"receiverOpenDingTalkId": "DAAAAAAAAAAAiE"},
},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
err := runChatCoverageCommand(t, caller, append([]string{"message", "send-card"}, tc.args...)...)
if err != nil {
t.Fatal(err)
}
if caller.calls != 1 || caller.server != "im" || caller.tool != "create_and_send_card" || !reflect.DeepEqual(caller.args, tc.want) {
t.Fatalf("call = count:%d server:%q tool:%q args:%#v, want %#v", caller.calls, caller.server, caller.tool, caller.args, tc.want)
}
})
}
}
func TestCrossPlatformCoverageChatGroupAuditJoinValidationUsesCanonicalAndAliasPayload(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs })
for _, tc := range []struct {
name string
flag string
}{
{name: "canonical conversation-id", flag: "--conversation-id=cid"},
{name: "hidden group alias", flag: "--group=cid"},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
err := runChatCoverageCommand(t, caller,
"group", "audit-join-validation",
tc.flag,
"--record-id=123",
"--applicant=D-applicant",
"--inviter=D-inviter",
"--status=AuditDelete",
"--description=deny",
)
if err != nil {
t.Fatal(err)
}
want := map[string]any{
"openConversationId": "cid",
"applyRecordId": int64(123),
"applicantUid": "D-applicant",
"inviterUid": "D-inviter",
"status": "AuditDelete",
"auditDescription": "deny",
}
if caller.calls != 1 || caller.server != "im" || caller.tool != "audit_join_group" || !reflect.DeepEqual(caller.args, want) {
t.Fatalf("call = count:%d server:%q tool:%q args:%#v, want %#v", caller.calls, caller.server, caller.tool, caller.args, want)
}
})
}
}
func TestCrossPlatformCoverageChatIMIDMigrationRequiredFlagErrors(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs })
tests := []struct {
name string
path []string
flag map[string]string
want string
}{
{name: "message list mutually exclusive targets", path: []string{"message", "list"}, flag: map[string]string{"conversation-id": "cid", "user": "u1", "time": "2026-01-01"}, want: "mutually exclusive"},
{name: "message list missing target", path: []string{"message", "list"}, flag: map[string]string{"time": "2026-01-01"}, want: "--conversation-id, --user or --open-dingtalk-id is required"},
{name: "topic replies missing conversation", path: []string{"message", "list-topic-replies"}, flag: map[string]string{"topic-id": "t1"}, want: "conversation-id"},
{name: "read status missing conversation", path: []string{"message", "read-status"}, flag: map[string]string{"message-id": "m1"}, want: "conversation-id"},
{name: "read status conflicting aliases", path: []string{"message", "read-status"}, flag: map[string]string{"conversation-id": "cid1", "group": "cid2", "message-id": "m1"}, want: "conflicts"},
{name: "read status missing message", path: []string{"message", "read-status"}, flag: map[string]string{"conversation-id": "cid"}, want: "message-id"},
{name: "update text emotion missing message", path: []string{"message", "update-text-emotion"}, flag: map[string]string{"conversation-id": "cid", "old-emotion-id": "e1", "emotion-id": "e2", "emotion-name": "n", "text": "t", "background-id": "b"}, want: "message-id"},
{name: "update text emotion missing detail flag", path: []string{"message", "update-text-emotion"}, flag: map[string]string{"conversation-id": "cid", "message-id": "m1", "old-emotion-id": "e1", "emotion-id": "e2", "emotion-name": "n", "text": "t"}, want: "background-id"},
{name: "transfer owner missing conversation", path: []string{"group", "transfer-owner"}, flag: map[string]string{"new-owner": "D1"}, want: "conversation-id"},
{name: "invite url missing conversation", path: []string{"group", "invite-url"}, want: "conversation-id"},
{name: "quit missing conversation", path: []string{"group", "quit"}, want: "conversation-id"},
{name: "update icon missing conversation", path: []string{"group", "update-icon"}, flag: map[string]string{"icon-media-id": "@media"}, want: "conversation-id"},
{name: "update settings missing conversation", path: []string{"group", "update-settings"}, flag: map[string]string{"setting-key": "searchable"}, want: "conversation-id"},
{name: "set admin missing conversation", path: []string{"group", "set-admin"}, flag: map[string]string{"users": "D1"}, want: "conversation-id"},
{name: "role list missing conversation", path: []string{"group-role", "list"}, want: "group"},
{name: "role add missing conversation", path: []string{"group-role", "add"}, flag: map[string]string{"name": "role"}, want: "conversation-id"},
{name: "role update missing conversation", path: []string{"group-role", "update"}, flag: map[string]string{"role-id": "r1", "name": "role"}, want: "conversation-id"},
{name: "role remove missing conversation", path: []string{"group-role", "remove"}, flag: map[string]string{"role-id": "r1"}, want: "conversation-id"},
{name: "role set user missing conversation", path: []string{"group-role", "set-user"}, flag: map[string]string{"user": "D1", "role-ids": "r1"}, want: "conversation-id"},
{name: "role remove user missing conversation", path: []string{"group-role", "remove-user"}, flag: map[string]string{"user": "D1", "role-ids": "r1"}, want: "conversation-id"},
{name: "role query user missing conversation", path: []string{"group-role", "query-user"}, flag: map[string]string{"user": "D1"}, want: "conversation-id"},
{name: "bots missing legacy group", path: []string{"group", "bots"}, want: "group"},
{name: "bots rejects migrated conversation id", path: []string{"group", "bots"}, flag: map[string]string{"conversation-id": "cid"}, want: "no such flag"},
{name: "dismiss missing conversation", path: []string{"group", "dismiss"}, flag: map[string]string{"yes": "true"}, want: "conversation-id"},
{name: "set history missing conversation", path: []string{"group", "set-history"}, flag: map[string]string{"option": "ALL"}, want: "conversation-id"},
{name: "set pin missing message", path: []string{"message", "set-pin-msg"}, flag: map[string]string{"open-conversation-id": "cid"}, want: "message-id"},
{name: "unset pin missing message", path: []string{"message", "unset-pin-msg"}, flag: map[string]string{"open-conversation-id": "cid"}, want: "message-id"},
{name: "audit join missing conversation", path: []string{"group", "audit-join-validation"}, flag: map[string]string{"record-id": "1", "applicant": "D1", "inviter": "D2", "status": "AuditApprove"}, want: "conversation-id"},
{name: "set top missing message", path: []string{"message", "set-top-msg"}, flag: map[string]string{"open-conversation-id": "cid"}, want: "message-id"},
{name: "unset top missing message", path: []string{"message", "unset-top-msg"}, flag: map[string]string{"open-conversation-id": "cid"}, want: "message-id"},
{name: "update alias missing conversation", path: []string{"group", "update-alias"}, flag: map[string]string{"alias-title": "alias"}, want: "conversation-id"},
{name: "notice create missing conversation", path: []string{"group", "notice", "create"}, flag: map[string]string{"content": "hello"}, want: "conversation-id"},
{name: "notice edit missing conversation", path: []string{"group", "notice", "edit"}, flag: map[string]string{"notice-id": "n1", "content": "hello"}, want: "conversation-id"},
{name: "notice get missing conversation", path: []string{"group", "notice", "get"}, flag: map[string]string{"notice-id": "n1"}, want: "conversation-id"},
{name: "notice list missing conversation", path: []string{"group", "notice", "list"}, want: "conversation-id"},
}
probe := newChatCommand()
messageList, _, err := probe.Find([]string{"message", "list"})
if err != nil {
t.Fatal(err)
}
if got, err := chatConversationID(messageList); err != nil || got != "" {
t.Fatalf("empty chatConversationID = %q, %v", got, err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := runChatCoverageDirect(t, tc.path, tc.flag)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want containing %q", err, tc.want)
}
})
}
}
func TestCrossPlatformCoverageChatMessageReadStatusConversationAliasesExecute(t *testing.T) {
for _, alias := range []string{"group", "id", "chat", "open-conversation-id"} {
t.Run(alias, func(t *testing.T) {
caller := &scriptedToolCaller{}
if err := runChatCoverageCommand(t, caller, "message", "read-status", "--"+alias, "cid-1", "--message-id", "msg-1"); err != nil {
t.Fatal(err)
}
if caller.server != "im" || caller.tool != "query_msg_read_status" {
t.Fatalf("call = %s/%s, want im/query_msg_read_status", caller.server, caller.tool)
}
if got := caller.args["openConversationId"]; got != "cid-1" {
t.Fatalf("openConversationId = %#v, want cid-1", got)
}
if got := caller.args["openMessageId"]; got != "msg-1" {
t.Fatalf("openMessageId = %#v, want msg-1", got)
}
})
}
}
func TestCrossPlatformCoverageChatWebhookReplyConversationAndDownloadEdges(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
@@ -343,6 +594,7 @@ func TestCrossPlatformCoverageChatWebhookReplyConversationAndDownloadEdges(t *te
_ = runChatCoverageCommand(t, &scriptedToolCaller{}, "conversation-info", "--open-dingtalk-id=D1")
_ = runChatCoverageCommand(t, &scriptedToolCaller{}, "conversation-info", "--user=D1")
_ = runChatCoverageCommand(t, &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"result":[{"userId":"u1","openDingTalkId":"D1"}]}`}, {text: `{}`}}}, "conversation-info", "--user=u1")
_ = runChatCoverageCommand(t, &scriptedToolCaller{}, "message", "send-card", "--open-dingtalk-id=D1")
_ = runChatCoverageCommand(t, &scriptedToolCaller{}, "message", "send-card", "--receiver=D1")
oldGet := httpGetFile
+170 -2
View File
@@ -17,8 +17,88 @@ import (
"bytes"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageChatGroupAuditJoinValidationAliasContract(t *testing.T) {
cmd := newChatCommand()
leaf, _, err := cmd.Find([]string{"group", "audit-join-validation"})
if err != nil {
t.Fatal(err)
}
canonical := leaf.Flags().Lookup("conversation-id")
if canonical == nil || canonical.Hidden {
t.Fatalf("conversation-id flag = %#v, want visible canonical", canonical)
}
legacy := leaf.Flags().Lookup("group")
if legacy == nil || !legacy.Hidden {
t.Fatalf("group flag = %#v, want hidden compatibility alias", legacy)
}
if got := legacy.Annotations[runtimeannotate.AnnotationFlagAliasOf]; len(got) != 1 || got[0] != "conversation-id" {
t.Fatalf("group alias_of annotation = %#v", got)
}
if got := legacy.Annotations[runtimeannotate.AnnotationFlagAliasOrigin]; len(got) != 1 || got[0] != runtimeannotate.FlagAliasOriginCorecmdV1 {
t.Fatalf("group alias_origin annotation = %#v", got)
}
if got := legacy.Annotations[cobra.BashCompOneRequiredFlag]; len(got) != 0 {
t.Fatalf("hidden group alias kept required annotation: %#v", got)
}
}
func TestCrossPlatformCoverageChatGroupAuditJoinValidationRestoreRequiredNoop(t *testing.T) {
restoreChatGroupBotsLegacyRequired(nil)
restoreChatPendingMigrationCanonicalRequired(nil)
root := &cobra.Command{Use: "chat"}
root.AddCommand(&cobra.Command{Use: "other"})
restoreChatGroupBotsLegacyRequired(root)
restoreChatPendingMigrationCanonicalRequired(root)
}
func TestCrossPlatformCoverageChatGroupBotsKeepsLegacyGroupFlag(t *testing.T) {
cmd := newChatCommand()
leaf, _, err := cmd.Find([]string{"group", "bots"})
if err != nil {
t.Fatal(err)
}
group := leaf.Flags().Lookup("group")
if group == nil || group.Hidden {
t.Fatalf("group flag = %#v, want visible legacy flag", group)
}
if got := group.Annotations[cobra.BashCompOneRequiredFlag]; len(got) == 0 || got[0] != "true" {
t.Fatalf("group required annotation = %#v, want true", got)
}
if leaf.Flags().Lookup("conversation-id") != nil {
t.Fatalf("chat group bots still exposes migrated --conversation-id")
}
if leaf.Flags().Lookup("group-name") != nil {
t.Fatalf("chat group bots still exposes migrated --group-name")
}
}
func TestCrossPlatformCoverageChatPendingMigrationAliasesMatchManifest(t *testing.T) {
cmd := newChatCommand()
leaf, _, err := cmd.Find([]string{"group", "dismiss"})
if err != nil {
t.Fatal(err)
}
canonical := leaf.Flags().Lookup("conversation-id")
if canonical == nil {
t.Fatal("missing conversation-id flag")
}
if got := canonical.Annotations[cobra.BashCompOneRequiredFlag]; len(got) == 0 || got[0] != "true" {
t.Fatalf("conversation-id required annotation = %#v, want true", got)
}
legacy := leaf.Flags().Lookup("group")
if legacy == nil || !legacy.Hidden {
t.Fatalf("group flag = %#v, want hidden legacy alias", legacy)
}
if got := legacy.Annotations[runtimeannotate.AnnotationFlagAliasOf]; len(got) != 1 || got[0] != "conversation-id" {
t.Fatalf("group alias_of annotation = %#v", got)
}
}
func TestCrossPlatformCoverageChatMessageHelpDocumentsPostSendIDChain(t *testing.T) {
tests := []struct {
name string
@@ -51,7 +131,7 @@ func TestCrossPlatformCoverageChatMessageHelpDocumentsPostSendIDChain(t *testing
contains: []string{
"send -> query-send-status -> edit",
"query-send-status --open-task-id <上一步返回的openTaskId>",
"edit --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>",
"edit --conversation-id <上一步返回的openConversationId> --message-id <上一步返回的openMessageId>",
},
notContain: "chat message list",
},
@@ -61,7 +141,7 @@ func TestCrossPlatformCoverageChatMessageHelpDocumentsPostSendIDChain(t *testing
contains: []string{
"send -> query-send-status -> recall",
"query-send-status --open-task-id <上一步返回的openTaskId>",
"recall --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>",
"recall --conversation-id <上一步返回的openConversationId> --message-id <上一步返回的openMessageId>",
},
notContain: "chat message list",
},
@@ -183,3 +263,91 @@ func TestCrossPlatformCoverageChatMessageHelpDocumentsOptionalTimeDefaults(t *te
})
}
}
func TestCrossPlatformCoverageChatReactionHelpKeepsManifestExternalAliasesVisible(t *testing.T) {
for _, command := range []string{"add-emoji", "remove-emoji", "add-text-emotion", "remove-text-emotion"} {
t.Run(command, func(t *testing.T) {
cmd := newChatCommand()
var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)
cmd.SetArgs([]string{"message", command, "--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("chat message %s --help: %v\n%s", command, err, output.String())
}
help := output.String()
if !strings.Contains(help, "--conversation-id") {
t.Fatalf("chat message %s help missing --conversation-id:\n%s", command, help)
}
for _, visible := range []string{"--group", "--id", "--chat"} {
if !strings.Contains(help, visible+" string") {
t.Fatalf("chat message %s help hides manifest-external alias %s:\n%s", command, visible, help)
}
}
})
}
}
func TestCrossPlatformCoverageChatGroupBotsHelpKeepsLegacyGroup(t *testing.T) {
cmd := newChatCommand()
var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)
cmd.SetArgs([]string{"group", "bots", "--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("chat group bots --help: %v\n%s", err, output.String())
}
help := output.String()
if !strings.Contains(help, "--group string") {
t.Fatalf("chat group bots help missing visible --group:\n%s", help)
}
for _, hidden := range []string{"--conversation-id", "--group-name"} {
if strings.Contains(help, hidden) {
t.Fatalf("chat group bots help exposes migrated flag %s:\n%s", hidden, help)
}
}
}
func TestCrossPlatformCoverageChatSendCardHelpUsesCanonicalIDFlags(t *testing.T) {
cmd := newChatCommand()
var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)
cmd.SetArgs([]string{"message", "send-card", "--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("chat message send-card --help: %v\n%s", err, output.String())
}
help := output.String()
for _, visible := range []string{"--conversation-id", "--open-dingtalk-id"} {
if !strings.Contains(help, visible) {
t.Fatalf("send-card help missing %s:\n%s", visible, help)
}
}
for _, visible := range []string{"--group", "--receiver"} {
if !strings.Contains(help, visible+" string") {
t.Fatalf("send-card help hides manifest-external alias %s:\n%s", visible, help)
}
}
}
func TestCrossPlatformCoverageChatGroupAuditJoinValidationHelpUsesCanonicalConversationID(t *testing.T) {
cmd := newChatCommand()
var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)
cmd.SetArgs([]string{"group", "audit-join-validation", "--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("chat group audit-join-validation --help: %v\n%s", err, output.String())
}
help := output.String()
if !strings.Contains(help, "--conversation-id") {
t.Fatalf("audit-join-validation help missing --conversation-id:\n%s", help)
}
if strings.Contains(help, "--group string") {
t.Fatalf("audit-join-validation help exposes hidden --group alias:\n%s", help)
}
}
+67 -19
View File
@@ -81,6 +81,32 @@ func TestCrossPlatformCoverageChatUpdateTextEmotion(t *testing.T) {
},
},
},
{
name: "open-conversation-id alias",
args: []string{
"message", "update-text-emotion",
"--open-conversation-id", "conv-3",
"--message-id", "msg-3",
"--old-emotion-id", "old-3",
"--emotion-id", "new-3",
"--emotion-name", "smile",
"--text", "done",
"--background-id", "im_bg_2",
},
want: guardedMutationCall{
productID: "im",
toolName: "update_text_emotion",
args: map[string]any{
"openConversationId": "conv-3",
"openMsgId": "msg-3",
"oldEmotionId": "old-3",
"emotionId": "new-3",
"emotionName": "smile",
"text": "done",
"backgroundId": "im_bg_2",
},
},
},
}
for _, test := range tests {
test := test
@@ -127,7 +153,7 @@ func TestCrossPlatformCoverageChatUpdateTextEmotionRequiredFlags(t *testing.T) {
{
name: "missing conversation-id and aliases",
args: dropFlag("--conversation-id"),
wantErr: "at least one of the flags in the group [conversation-id group id chat] is required",
wantErr: "missing required flag: --conversation-id (or --group / --id / --chat / --open-conversation-id)",
},
{
name: "missing old-emotion-id",
@@ -135,7 +161,7 @@ func TestCrossPlatformCoverageChatUpdateTextEmotionRequiredFlags(t *testing.T) {
wantErr: `required flag(s) "old-emotion-id" not set`,
},
{
name: "missing msg-id and background-id",
name: "missing message-id and background-id",
args: []string{
"message", "update-text-emotion",
"--conversation-id", "conv-1",
@@ -144,7 +170,7 @@ func TestCrossPlatformCoverageChatUpdateTextEmotionRequiredFlags(t *testing.T) {
"--emotion-name", "like",
"--text", "nice",
},
wantErr: `required flag(s) "background-id", "msg-id" not set`,
wantErr: "missing required flag: --message-id (or --msg-id / --open-message-id)",
},
}
for _, test := range tests {
@@ -163,19 +189,41 @@ func TestCrossPlatformCoverageChatUpdateTextEmotionRequiredFlags(t *testing.T) {
}
func TestCrossPlatformCoverageChatGroupGetMuteConfig(t *testing.T) {
caller := &guardedMutationCaller{}
err := executeGuardedMutationCommand(t, caller, newChatCommand,
"group", "get-mute-config", "--group", "conv-1")
if err != nil {
t.Fatalf("get-mute-config returned error: %v", err)
}
want := guardedMutationCall{
productID: "im",
toolName: "get_group_mute_config",
args: map[string]any{"openConversationId": "conv-1"},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("tool calls = %#v, want %#v", caller.calls, want)
for _, test := range []struct {
name string
args []string
want guardedMutationCall
}{
{
name: "legacy group alias",
args: []string{"group", "get-mute-config", "--group", "conv-1"},
want: guardedMutationCall{
productID: "im",
toolName: "get_group_mute_config",
args: map[string]any{"openConversationId": "conv-1"},
},
},
{
name: "canonical conversation id",
args: []string{"group", "get-mute-config", "--conversation-id", "conv-2"},
want: guardedMutationCall{
productID: "im",
toolName: "get_group_mute_config",
args: map[string]any{"openConversationId": "conv-2"},
},
},
} {
test := test
t.Run(test.name, func(t *testing.T) {
caller := &guardedMutationCaller{}
err := executeGuardedMutationCommand(t, caller, newChatCommand, test.args...)
if err != nil {
t.Fatalf("get-mute-config returned error: %v", err)
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], test.want) {
t.Fatalf("tool calls = %#v, want %#v", caller.calls, test.want)
}
})
}
}
@@ -196,12 +244,12 @@ func TestCrossPlatformCoverageChatGroupGetMuteConfigRecordsRawArgs(t *testing.T)
}
}
func TestCrossPlatformCoverageChatGroupGetMuteConfigRequiresGroup(t *testing.T) {
func TestCrossPlatformCoverageChatGroupGetMuteConfigRequiresConversationID(t *testing.T) {
caller := &guardedMutationCaller{}
err := executeGuardedMutationCommand(t, caller, newChatCommand,
"group", "get-mute-config")
if err == nil || !strings.Contains(err.Error(), "--group") {
t.Fatalf("err = %v, want message containing --group", err)
if err == nil || !strings.Contains(err.Error(), "--conversation-id") {
t.Fatalf("err = %v, want message containing --conversation-id", err)
}
if len(caller.calls) != 0 {
t.Fatalf("tool calls = %#v, want none", caller.calls)
@@ -116,8 +116,8 @@ func requireWukongWeeklySyncConfirmation(t *testing.T, err error) {
func TestCrossPlatformCoverageWukongWeeklyChatCategoryQueries(t *testing.T) {
caller := &wukongWeeklySyncCaller{}
_, _, err := executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, "category", "list-by-conv")
if err == nil || !strings.Contains(err.Error(), "flag --group is required") {
t.Fatalf("missing group error = %v", err)
if err == nil || !strings.Contains(err.Error(), "missing required flag: --conversation-id") {
t.Fatalf("missing conversation ID error = %v", err)
}
requireWukongWeeklySyncNoCalls(t, caller)
@@ -378,15 +378,15 @@ func TestCrossPlatformCoverageWukongWeeklyChatUpdateNickClearSemantics(t *testin
if findErr != nil {
t.Fatal(findErr)
}
if err := updateNick.RunE(updateNick, nil); err == nil || !strings.Contains(err.Error(), "--group") {
t.Fatalf("direct missing group error = %v", err)
if err := updateNick.RunE(updateNick, nil); err == nil || !strings.Contains(err.Error(), "--conversation-id") {
t.Fatalf("direct missing conversation-id error = %v", err)
}
caller := &wukongWeeklySyncCaller{}
_, _, err := executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand,
"group", "update-nick")
if err == nil || !strings.Contains(err.Error(), "group") {
t.Fatalf("missing group error = %v", err)
if err == nil || !strings.Contains(err.Error(), "conversation-id") {
t.Fatalf("missing conversation-id error = %v", err)
}
requireWukongWeeklySyncNoCalls(t, caller)
@@ -428,8 +428,8 @@ func TestCrossPlatformCoverageWukongWeeklyChatUpgradeValidationAndSafety(t *test
upgrade.Flags().Bool("yes", false, "")
}
_ = upgrade.Flags().Set("yes", "true")
if err := upgrade.RunE(upgrade, nil); err == nil || !strings.Contains(err.Error(), "--group") {
t.Fatalf("direct missing group error = %v", err)
if err := upgrade.RunE(upgrade, nil); err == nil || !strings.Contains(err.Error(), "--conversation-id") {
t.Fatalf("direct missing conversation-id error = %v", err)
}
tests := []struct {
+103
View File
@@ -7,6 +7,7 @@ MODULE="$(cd "$ROOT" && go list -m)"
usage() {
printf '%s\n' \
"usage: $0 list <app|generators|helpers|cli|smoke|remaining|release-scripts>" \
" $0 list-coverage <app|cli|generators|helpers|remaining>" \
" $0 verify" >&2
exit 2
}
@@ -57,6 +58,52 @@ list_shard() {
esac
}
# Full-suite coverage measurement shards the same authoritative package set the
# single serial run used: ./ ./cmd/... ./internal/... ./skills/... (pkg/ and
# scripts/ belong to the supporting profile; test/ suites carry no production
# statements for the candidate profile). Shards stay disjoint so their merged
# profile is block-for-block equivalent to one serial invocation.
list_coverage_scope() {
cd "$ROOT"
go list ./ ./cmd/... ./internal/... ./skills/...
}
list_coverage_shard() {
shard="$1"
cd "$ROOT"
case "$shard" in
app)
go list ./internal/app/...
;;
cli)
go list ./internal/cli/...
;;
generators)
go list ./internal/generator/...
;;
helpers)
go list ./internal/helpers/...
;;
remaining)
scope_packages="$(list_coverage_scope)"
printf '%s\n' "$scope_packages" | while IFS= read -r package; do
case "$package" in
"$MODULE/internal/app"|"$MODULE/internal/app/"*) ;;
"$MODULE/internal/cli"|"$MODULE/internal/cli/"*) ;;
"$MODULE/internal/generator"|"$MODULE/internal/generator/"*) ;;
"$MODULE/internal/helpers"|"$MODULE/internal/helpers/"*) ;;
*) printf '%s\n' "$package" ;;
esac
done
;;
*)
printf 'unknown coverage 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
@@ -114,6 +161,58 @@ verify_plan() {
package_count="$(wc -l < "$expected" | tr -d ' ')"
printf 'test package plan covers %s default packages exactly once\n' "$package_count"
coverage_expected="$workdir/coverage-expected"
coverage_assigned="$workdir/coverage-assigned"
coverage_unique="$workdir/coverage-unique"
coverage_duplicates="$workdir/coverage-duplicates"
coverage_missing="$workdir/coverage-missing"
coverage_unexpected="$workdir/coverage-unexpected"
list_coverage_scope > "$workdir/coverage-scope"
LC_ALL=C sort -u "$workdir/coverage-scope" > "$coverage_expected"
: > "$coverage_assigned"
for shard in app cli generators helpers remaining; do
shard_packages="$workdir/coverage-$shard"
unsorted_shard_packages="$workdir/coverage-$shard.unsorted"
list_coverage_shard "$shard" > "$unsorted_shard_packages"
LC_ALL=C sort "$unsorted_shard_packages" > "$shard_packages"
if [ ! -s "$shard_packages" ]; then
printf 'coverage package shard is empty: %s\n' "$shard" >&2
exit 1
fi
cat "$shard_packages" >> "$coverage_assigned"
done
LC_ALL=C sort "$coverage_assigned" -o "$coverage_assigned"
uniq -d "$coverage_assigned" > "$coverage_duplicates"
LC_ALL=C sort -u "$coverage_assigned" > "$coverage_unique"
comm -23 "$coverage_expected" "$coverage_unique" > "$coverage_missing"
comm -13 "$coverage_expected" "$coverage_unique" > "$coverage_unexpected"
coverage_failed=0
if [ -s "$coverage_duplicates" ]; then
printf '%s\n' 'coverage packages assigned to more than one shard:' >&2
sed 's/^/ /' "$coverage_duplicates" >&2
coverage_failed=1
fi
if [ -s "$coverage_missing" ]; then
printf '%s\n' 'full-suite coverage packages missing from the CI coverage plan:' >&2
sed 's/^/ /' "$coverage_missing" >&2
coverage_failed=1
fi
if [ -s "$coverage_unexpected" ]; then
printf '%s\n' 'CI coverage plan contains packages outside its scope:' >&2
sed 's/^/ /' "$coverage_unexpected" >&2
coverage_failed=1
fi
if [ "$coverage_failed" -ne 0 ]; then
exit 1
fi
coverage_count="$(wc -l < "$coverage_expected" | tr -d ' ')"
printf 'coverage package plan covers %s full-suite packages exactly once\n' "$coverage_count"
}
case "${1:-}" in
@@ -121,6 +220,10 @@ case "${1:-}" in
[ "$#" -eq 2 ] || usage
list_shard "$2"
;;
list-coverage)
[ "$#" -eq 2 ] || usage
list_coverage_shard "$2"
;;
verify)
[ "$#" -eq 1 ] || usage
verify_plan
@@ -31,7 +31,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -97,7 +97,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -130,7 +130,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -163,7 +163,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -196,7 +196,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -229,7 +229,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -262,7 +262,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -295,7 +295,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -328,7 +328,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -361,7 +361,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -394,7 +394,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -427,7 +427,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -460,7 +460,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -493,7 +493,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -526,7 +526,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -559,7 +559,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -592,7 +592,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -625,7 +625,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -658,7 +658,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -691,7 +691,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -724,7 +724,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -757,7 +757,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -790,7 +790,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -823,7 +823,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -856,7 +856,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -892,7 +892,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -925,7 +925,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -991,7 +991,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -1024,7 +1024,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -1057,7 +1057,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -1090,7 +1090,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
@@ -1123,7 +1123,7 @@
"required": true
}
},
"state": "pending",
"state": "consumed",
"reason": "保留旧 argv 兼容性,并将 IM ID 规范 flag 设为唯一可见入口"
},
{
+33
View File
@@ -14,6 +14,39 @@ func TestCITestPackagePlanCoversDefaultPackagesExactlyOnce(t *testing.T) {
if !strings.Contains(output, "default packages exactly once") {
t.Fatalf("verify output = %q, want coverage summary", output)
}
if !strings.Contains(output, "full-suite packages exactly once") {
t.Fatalf("verify output = %q, want coverage shard plan summary", output)
}
}
func TestCICoveragePackagePlanRoutesFullSuiteScope(t *testing.T) {
root := testPackagePlanRoot(t)
remaining := strings.Fields(runTestPackagePlan(t, root, "list-coverage", "remaining"))
for _, suffix := range []string{"/cmd", "/internal/output", "/skills"} {
if !containsPackageSuffix(remaining, suffix) {
t.Errorf("coverage remaining shard does not contain package ending in %q", suffix)
}
}
for _, suffix := range []string{
"/internal/app",
"/internal/cli",
"/internal/generator",
"/internal/helpers",
"/test/smoke",
"/test/scripts",
"/pkg/cmdutil",
"/scripts/policy/coverage-gate",
} {
if containsPackageSuffix(remaining, suffix) {
t.Errorf("coverage remaining shard unexpectedly contains package ending in %q", suffix)
}
}
app := strings.Fields(runTestPackagePlan(t, root, "list-coverage", "app"))
if !containsPackageSuffix(app, "/internal/app") {
t.Error("coverage app shard does not contain /internal/app")
}
}
func TestCITestPackagePlanRoutesPublicTestSuites(t *testing.T) {
@@ -118,3 +118,108 @@ esac
assertDiffProfiles(t, runGate(t, &empty), "coverage.txt")
})
}
// TestCoverageWorkflowShardsAndBaselineCache pins the full-suite coverage
// architecture: the candidate profile is produced by disjoint per-shard
// helper jobs and reassembled before enforcement, and the merge-base profile
// is reused only through an exact-key cache written by a green main push of
// that same commit. Near-miss reuse (restore-keys) would compare the
// candidate against the wrong commit and must never appear.
func TestCoverageWorkflowShardsAndBaselineCache(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
data, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "ci.yml"))
if err != nil {
t.Fatalf("ReadFile(ci.yml) error = %v", err)
}
admission := string(data)
currentStart := strings.Index(admission, "\n coverage-current:\n")
fullStart := strings.Index(admission, "\n coverage-current-full:\n")
supportingStart := strings.Index(admission, "\n coverage-supporting:\n")
baselineStart := strings.Index(admission, "\n coverage-baseline:\n")
gateStart := strings.Index(admission, "\n coverage:\n")
policyStart := strings.Index(admission, "\n policy:\n")
if currentStart < 0 || fullStart <= currentStart || supportingStart <= fullStart ||
baselineStart <= supportingStart || gateStart <= baselineStart || policyStart <= gateStart {
t.Fatal("CI workflow missing ordered coverage job boundaries")
}
currentJob := admission[currentStart:fullStart]
if !strings.Contains(currentJob, "needs.lint.outputs.full_suite != 'true'") {
t.Error("coverage-current must be scoped-tier only; the full suite belongs to the shard matrix")
}
if strings.Contains(currentJob, "./ ./cmd/... ./internal/... ./skills/...") {
t.Error("coverage-current must not retain the retired single serial full-suite run")
}
fullJob := admission[fullStart:supportingStart]
for _, want := range []string{
"needs.lint.outputs.full_suite == 'true'",
"fail-fast: false",
" - app",
" - cli",
" - generators",
" - helpers",
" - remaining",
`./scripts/ci/test-packages.sh list-coverage "$COVERAGE_SHARD"`,
"go test -count=1 -p 1",
`-coverprofile="coverage-shard-$COVERAGE_SHARD.txt"`,
"-covermode=atomic",
"name: coverage-current-shard-${{ matrix.shard }}",
} {
if !strings.Contains(fullJob, want) {
t.Errorf("coverage-current-full missing shard contract %q", want)
}
}
baselineJob := admission[baselineStart:gateStart]
cachePath := "coverage-cache.txt"
baselineKey := "dws-coverage-full-v2-${{ env.COVERAGE_BASE_REF }}-go${{ steps.setup-go.outputs.go-version }}"
for _, want := range []string{
"uses: actions/cache/restore@v4",
"uses: actions/cache/save@v4",
"path: " + cachePath,
"key: " + baselineKey,
"if: steps.baseline-cache.outputs.cache-hit != 'true'",
"cp coverage-cache.txt coverage-base.txt",
"cp coverage-base.txt coverage-cache.txt",
} {
if !strings.Contains(baselineJob, want) {
t.Errorf("coverage-baseline missing cache contract %q", want)
}
}
if strings.Count(baselineJob, "key: "+baselineKey) != 2 {
t.Error("coverage-baseline restore and save must use the identical exact cache key")
}
if strings.Count(baselineJob, "path: "+cachePath) != 2 {
t.Error("coverage-baseline restore and save must use the identical cache path/version")
}
if strings.Contains(baselineJob, "restore-keys") {
t.Error("coverage baseline cache must stay exact-key; prefix restore-keys can resurrect a wrong-commit baseline")
}
gateJob := admission[gateStart:policyStart]
for _, want := range []string{
"pattern: coverage-current-*",
"merge-multiple: true",
"for shard in app cli generators helpers remaining; do",
"test ! -f coverage.txt",
`test "$(head -n 1 "$profile")" = "mode: atomic"`,
"github.event_name == 'push'",
"cp coverage.txt coverage-cache.txt",
"path: " + cachePath,
"key: dws-coverage-full-v2-${{ github.sha }}-go${{ steps.setup-go.outputs.go-version }}",
`"current shards:$CURRENT_FULL_RESULT:$current_full_expected"`,
} {
if !strings.Contains(gateJob, want) {
t.Errorf("coverage gate missing shard assembly contract %q", want)
}
}
if strings.Count(gateJob, "path: "+cachePath) != 1 {
t.Error("green main push must save the candidate profile through the same cache path/version as baseline restore")
}
}