diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba5d31ab..4707eb50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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[@]}" diff --git a/internal/app/param_alias_payload_equivalence_test.go b/internal/app/param_alias_payload_equivalence_test.go index a4fe2e16..eaa2a0b7 100644 --- a/internal/app/param_alias_payload_equivalence_test.go +++ b/internal/app/param_alias_payload_equivalence_test.go @@ -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 +} diff --git a/test/scripts/changelog_pr_gate_test.go b/test/scripts/changelog_pr_gate_test.go index 93a73f07..d01f366e 100644 --- a/test/scripts/changelog_pr_gate_test.go +++ b/test/scripts/changelog_pr_gate_test.go @@ -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",