test: harden parameter alias safety gates
This commit is contained in:
@@ -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 := ¶mAliasCaptureCaller{}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user