test: harden parameter alias safety gates

This commit is contained in:
克谨
2026-08-12 19:05:11 +08:00
parent 65bedd5f8c
commit b29a12abbf
3 changed files with 100 additions and 5 deletions
+6 -3
View File
@@ -537,10 +537,13 @@ jobs:
test -n "$package_output"
mapfile -t packages <<< "$package_output"
test "${#packages[@]}" -gt 0
# cli/smoke own heavy NewRootCommand / Schema assembly under -race; give
# them a dedicated budget so remaining is not SIGTERM'd by OOM/timeout.
# app/cli/smoke own heavy NewRootCommand / Schema assembly under
# -race; give them a dedicated budget so slower hosted runners do not
# hit the package timeout while the shards remain independently bounded.
timeout_budget=12m
if [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then
if [ "$TEST_SHARD" = "app" ] ||
[ "$TEST_SHARD" = "cli" ] ||
[ "$TEST_SHARD" = "smoke" ]; then
timeout_budget=15m
fi
go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}"
@@ -4,11 +4,13 @@
package app
import (
"errors"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline"
)
@@ -365,6 +367,24 @@ var paramAliasNewDriveCases = []struct {
{command: "drive +upload", emitted: "file-id", canonical: "node"},
}
// paramAliasNewDriveConfirmationCases selects one newly reviewed alias for
// every Drive command in the expansion whose declared runtime safety requires
// confirmation. The full matrix below proves all spellings preserve the
// confirmed payload; this smaller matrix proves aliases cannot cross the
// confirmation boundary before any transport call is made.
var paramAliasNewDriveConfirmationCases = []struct {
command string
emitted string
canonical string
}{
{command: "drive +delete", emitted: "file-id", canonical: "node"},
{command: "drive +publish-unset", emitted: "document-url", canonical: "node"},
{command: "drive +recycle-restore", emitted: "recycle-item-id", canonical: "id"},
{command: "drive +rename", emitted: "new-name", canonical: "name"},
{command: "drive +upload", emitted: "source-file", canonical: "file"},
{command: "drive +version-revert", emitted: "version-number", canonical: "version"},
}
// paramAliasRepresentativePayloadCases keeps final transport coverage across
// old concept aliases, command overrides, native compatibility flags, read and
// write commands, and different products. Every reviewed alias is still
@@ -602,6 +622,45 @@ func TestCrossPlatformCoverageNewDriveParamAliasesReachCanonicalEquivalentFinalP
}
}
func TestCrossPlatformCoverageNewDriveParamAliasesCannotBypassConfirmation(t *testing.T) {
for _, test := range paramAliasNewDriveConfirmationCases {
test := test
t.Run(test.command+"/"+test.emitted, func(t *testing.T) {
complete, ok := paramAliasCompleteCommand(test.command, test.canonical)
if !ok {
t.Fatal("reviewed Drive confirmation alias has no complete-command E2E template")
}
aliasArgs, replacements := replaceLongFlag(complete, test.canonical, test.emitted)
if replacements != 1 {
t.Fatalf("complete command must contain canonical --%s exactly once; replacements=%d args=%v", test.canonical, replacements, complete)
}
unconfirmedArgs, removals := removeExactArg(aliasArgs, "--yes")
if removals != 1 {
t.Fatalf("confirmation template must contain --yes exactly once; removals=%d args=%v", removals, aliasArgs)
}
entry, exists := cli.LookupParamAlias(test.command)
target, active := entry.ResolveAlias(test.emitted)
if !exists || !active || target != test.canonical {
t.Fatalf("reviewed Drive alias --%s resolution = exists:%v active:%v target:%q, want --%s", test.emitted, exists, active, target, test.canonical)
}
caller := &paramAliasCaptureCaller{}
ctx, err := executeParamAliasPayloadE2E(t, caller, unconfirmedArgs...)
if ctx == nil {
t.Fatal("unconfirmed alias command skipped PreParse")
}
var appErr *apperrors.Error
if !errors.As(err, &appErr) || appErr.Reason != "confirmation_required" {
t.Fatalf("unconfirmed alias command error = %#v, want confirmation_required\nargs=%v", err, unconfirmedArgs)
}
if len(caller.calls) != 0 {
t.Fatalf("unconfirmed alias command crossed the transport boundary: args=%v calls=%#v", unconfirmedArgs, caller.calls)
}
})
}
}
// Some artifact commands deliberately continue past the final captured
// transport call into local download/upload handling. Deterministic invalid
// resource responses form their expected post-transport test boundary;
@@ -674,3 +733,16 @@ func replaceLongFlag(args []string, canonical, emitted string) ([]string, int) {
}
return out, replacements
}
func removeExactArg(args []string, target string) ([]string, int) {
out := make([]string, 0, len(args))
removals := 0
for _, arg := range args {
if arg == target {
removals++
continue
}
out = append(out, arg)
}
return out, removals
}
+22 -2
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
)
type changelogGateRepo struct {
@@ -71,6 +72,20 @@ func newChangelogGateRepo(t *testing.T) *changelogGateRepo {
t.Fatalf("Abs(repo root) error = %v", err)
}
root := t.TempDir()
// Git may briefly continue automatic repository maintenance after a command
// exits on hosted runners. Proactively remove the fixture with bounded
// retries before testing.TempDir performs its strict final cleanup.
t.Cleanup(func() {
var cleanupErr error
for attempt := 0; attempt < 10; attempt++ {
cleanupErr = os.RemoveAll(root)
if cleanupErr == nil {
return
}
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
}
t.Errorf("RemoveAll changelog gate repository after retries: %v", cleanupErr)
})
for _, path := range []string{
"LICENSE",
@@ -108,6 +123,8 @@ func newChangelogGateRepo(t *testing.T) *changelogGateRepo {
changelogGateWrite(t, root, "CHANGELOG.md", changelogGateBase, 0o644)
changelogGateGit(t, root, "init", "-b", "main")
changelogGateGit(t, root, "config", "gc.auto", "0")
changelogGateGit(t, root, "config", "maintenance.auto", "false")
changelogGateGit(t, root, "config", "user.name", "Changelog Gate Test")
changelogGateGit(t, root, "config", "user.email", "changelog-gate@example.com")
changelogGateGit(t, root, "add", ".")
@@ -1166,10 +1183,13 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
}
raceJob := admission[raceStart:raceEnd]
// Full race shards use a dynamic package-level timeout: default/floor 12m,
// with cli/smoke raised to 15m for NewRootCommand / Schema assembly under -race.
// with app/cli/smoke raised to 15m for NewRootCommand / Schema assembly under
// -race on slower hosted runners.
for _, want := range []string{
"timeout_budget=12m",
`if [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then`,
`if [ "$TEST_SHARD" = "app" ] ||`,
`[ "$TEST_SHARD" = "cli" ] ||`,
`[ "$TEST_SHARD" = "smoke" ]; then`,
"timeout_budget=15m",
`go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}"`,
"- smoke",