Compare commits

...
Author SHA1 Message Date
克谨 b57cecb62d feat(cli): optimize eight shortcut parameter aliases 2026-08-24 17:51:45 +08:00
Dennis4477 8dc52d052c Merge pull request #1113 from DingTalk-Real-AI/codex/devapp-availability-compat-governance
policy: add consumable Schema availability migrations
2026-08-24 17:39:07 +08:00
Dennis 6147021ae8 policy: add consumable schema availability migrations 2026-08-24 16:47:19 +08:00
赤川 ae31d371de Merge pull request #1114 from DingTalk-Real-AI/codex/fix-policy-timeout-governance
ci: extend Policy job timeout for large Schema surfaces
2026-08-24 16:46:22 +08:00
chichuan c20a53cd8a ci: extend Policy job timeout 2026-08-24 16:43:10 +08:00
github-actions[bot] 1bb56364c2 Merge pull request #1111 from DingTalk-Real-AI/codex/ci-windows-coverage-governance
ci: bound exhaustive coverage test runtimes
2026-08-24 15:54:27 +08:00
克谨 758b0f875e ci: bound exhaustive coverage test runtimes 2026-08-24 15:28:00 +08:00
11 changed files with 2219 additions and 688 deletions
+8 -3
View File
@@ -544,7 +544,8 @@ jobs:
- app-schema
- app-a-b
- app-c-a-l
- app-c-m-r
- app-c-m-o
- app-c-p-r
- app-c-s-z
- app-c-other
- app-d-r
@@ -692,7 +693,8 @@ jobs:
- app-schema
- app-a-b
- app-c-a-l
- app-c-m-r
- app-c-m-o
- app-c-p-r
- app-c-s-z
- app-c-other
- app-d-r
@@ -1597,7 +1599,10 @@ jobs:
name: Policy
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 10
# Full policy regenerates and validates the runtime Schema several times.
# Keep job-level headroom for large reviewed command-surface additions;
# individual policy gates retain their own fail-closed checks.
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -153,7 +153,7 @@ var paramAliasCompleteCommands = map[string][]string{
"devdoc article search": {"devdoc", "article", "search", "--query", "fixture", "--page", "2", "--size", "7"},
"ding +receiver-status": {"ding", "+receiver-status", "--ding-id", "ding-1"},
"ding message receiver-status": {"ding", "message", "receiver-status", "--ding-id", "ding-1"},
"ding message send": {"ding", "message", "send", "--robot-code", "robot-1", "--content", "fixture", "--users", "user-1", "--yes"},
"ding message send": {"ding", "message", "send", "--robot-code", "robot-1", "--content", "fixture", "--users", "user-1"},
"doc +comment-create": {"doc", "+comment-create", "--node", "node-1", "--content", "fixture comment", "--yes"},
"doc +comment-list": {"doc", "+comment-list", "--node", "node-1", "--limit", "7", "--cursor", "cursor-1"},
"doc +comment-reply": {"doc", "+comment-reply", "--node", "node-1", "--comment-key", "comment-1", "--content", "fixture reply", "--yes"},
@@ -221,8 +221,8 @@ var paramAliasCompleteCommands = map[string][]string{
"drive search": {"drive", "search", "--query", "fixture", "--created-from", "1", "--created-to", "2", "--modified-from", "3", "--modified-to", "4", "--creator-uids", "user-1,user-2"},
"drive upload": {"drive", "upload", "--file", "../../go.mod", "--space-id", "space-1"},
"drive upload-info": {"drive", "upload-info", "--file-name", "fixture.txt", "--file-size", "7", "--space-id", "space-1"},
"mail +find-mail-user": {"mail", "+find-mail-user", "--query", "fixture", "--limit", "7"},
"mail folder update": {"mail", "folder", "update", "--email", "fixture@example.com", "--id", "folder-1", "--name", "Fixture Folder", "--yes"},
"mail +find-mail-user": {"mail", "+find-mail-user", "--query", "fixture", "--limit", "7", "--cursor", "cursor-1"},
"mail folder update": {"mail", "folder", "update", "--email", "fixture@example.com", "--id", "folder-1", "--name", "Fixture Folder"},
"mail message search": {"mail", "message", "search", "--email", "fixture@example.com", "--query", "subject:fixture"},
"mail thread list": {"mail", "thread", "list", "--email", "fixture@example.com", "--folder", "folder-1", "--limit", "7"},
"mail user search": {"mail", "user", "search", "--keyword", "fixture"},
@@ -231,12 +231,59 @@ var paramAliasCompleteCommands = map[string][]string{
"report list": {"report", "list", "--start", "2026-03-10T00:00:00+08:00", "--end", "2026-03-10T23:59:59+08:00"},
}
// paramAliasCandidateCompleteCommands contains complete invocations for the
// reviewed Minutes/TODO/Wiki joint draft. Keeping candidate-only commands in a
// separate map lets this test file land before the draft replaces the formal
// paramAliasCandidateCompleteCommands contains complete invocations for
// reviewed parameter-concept product drafts. Keeping candidate-only commands
// in a separate map lets a test change land before a draft replaces the formal
// param_concepts.json: inactive candidate templates are ignored, while every
// command becomes mandatory as soon as one of its reviewed aliases is active.
var paramAliasCandidateCompleteCommands = map[string][]string{
"attendance +check-record": {"attendance", "+check-record", "--users", "user-1,user-2", "--start", "2026-03-10 00:00:00", "--end", "2026-03-10 23:59:59"},
"attendance +get-adjustment-rule": {"attendance", "+get-adjustment-rule", "--adjustment-id", "adjustment-1"},
"attendance +get-approve-template": {"attendance", "+get-approve-template", "--type", "leave"},
"attendance +get-checkin-record": {"attendance", "+get-checkin-record", "--operator-corp-id", "corp-1", "--operator-staff-id", "staff-operator", "--staff-ids", "staff-1,staff-2", "--start", "2026-03-10 00:00:00", "--end", "2026-03-10 23:59:59"},
"attendance +get-leave-records": {"attendance", "+get-leave-records", "--user", "user-1", "--start", "2026-03-01", "--end", "2026-03-31", "--leave-code", "annual_leave"},
"attendance +get-overtime-rule": {"attendance", "+get-overtime-rule", "--overtime-id", "overtime-1"},
"attendance +get-schedule": {"attendance", "+get-schedule", "--users", "user-1,user-2", "--start", "2026-03-10", "--end", "2026-03-11"},
"attendance +get-self-setting": {"attendance", "+get-self-setting", "--user", "user-1", "--setting-scene", "checkRemind"},
"attendance +get-summary": {"attendance", "+get-summary", "--user", "user-1", "--date", "2026-03-10", "--stats-type", "week"},
"attendance +list-approve": {"attendance", "+list-approve", "--users", "user-1,user-2", "--types", "leave", "--start", "2026-03-01", "--end", "2026-03-31"},
"attendance +query-report-data": {"attendance", "+query-report-data", "--users", "user-1,user-2", "--columns", "attendance_days,late_count", "--start", "2026-03-01", "--end", "2026-03-31"},
"attendance +search-adjustment-rule": {"attendance", "+search-adjustment-rule", "--query", "fixture", "--page", "2", "--limit", "7"},
"attendance +search-class": {"attendance", "+search-class", "--filter-type", "name", "--query", "fixture"},
"attendance +search-group": {"attendance", "+search-group", "--type", "FIXED"},
"attendance +search-overtime-rule": {"attendance", "+search-overtime-rule", "--query", "fixture", "--page", "2", "--limit", "7"},
"ding +list": {"ding", "+list", "--cursor", "0", "--type", "ALL"},
"ding +recall-personal": {"ding", "+recall-personal", "--id", "ding-1", "--yes"},
"ding +send-personal": {"ding", "+send-personal", "--users", appFixtureCurrentDOpenID, "--content", "fixture", "--yes"},
"mail +contact-list": {"mail", "+contact-list", "--email", "fixture@example.com", "--limit", "7", "--cursor", "cursor-1"},
"mail +folder-list": {"mail", "+folder-list", "--email", "fixture@example.com", "--folder", "folder-1"},
"mail +message": {"mail", "+message", "--email", "fixture@example.com", "--id", "message-1"},
"mail +messages": {"mail", "+messages", "--email", "fixture@example.com", "--ids", "message-1,message-2"},
"mail +recent-mail": {"mail", "+recent-mail", "--limit", "7", "--cursor", "cursor-1"},
"mail +search-mail": {"mail", "+search-mail", "--query", "fixture", "--size", "7", "--cursor", "cursor-1"},
"mail +template-list": {"mail", "+template-list", "--email", "fixture@example.com", "--limit", "7", "--cursor", "cursor-1"},
"mail +thread": {"mail", "+thread", "--email", "fixture@example.com", "--id", "thread-1"},
"mail +thread-list": {"mail", "+thread-list", "--email", "fixture@example.com", "--folder", "folder-1", "--cursor", "cursor-1"},
"mail +triage": {"mail", "+triage", "--query", "fixture", "--limit", "7", "--cursor", "cursor-1"},
"mail +unread-mail": {"mail", "+unread-mail", "--size", "7", "--cursor", "cursor-1"},
"mail +user-search": {"mail", "+user-search", "--keyword", "fixture", "--cursor", "cursor-1"},
"markdown create": {"markdown", "create", "--content", "# Fixture", "--name", "fixture.md", "--space-id", "space-1"},
"markdown diff": {"markdown", "diff", "--node", "node-1", "--version", "1", "--version2", "2", "--context", "3"},
"markdown fetch": {"markdown", "fetch", "--node", "node-1", "--space-id", "space-1", "--output", "/tmp/dws-markdown-fixture.md"},
"markdown overwrite": {"markdown", "overwrite", "--node", "node-1", "--content", "# Fixture", "--name", "fixture.md", "--space-id", "space-1", "--yes"},
"markdown patch": {"markdown", "patch", "--node", "node-1", "--pattern", "old", "--content", "new", "--regex", "--space-id", "space-1", "--yes"},
"oa +list-cc": {"oa", "+list-cc", "--page", "2"},
"oa +list-executed": {"oa", "+list-executed", "--limit", "7", "--page", "2"},
"oa +list-forms": {"oa", "+list-forms", "--cursor", "2"},
"oa +list-pending": {"oa", "+list-pending", "--start", "1773072000000", "--end", "1773158399000", "--page", "2"},
"oa +list-submitted": {"oa", "+list-submitted", "--page", "2"},
"oa +my-initiated": {"oa", "+my-initiated", "--page", "2"},
"report +outbox-list": {"report", "+outbox-list", "--size", "7"},
"report +report-latest": {"report", "+report-latest", "--keyword", "Fixture", "--start", "2026-03-01T00:00:00+08:00", "--end", "2026-03-10T00:00:00+08:00"},
"report +template-search": {"report", "+template-search", "--query", "fixture"},
"sheet +list-sheets": {"sheet", "+list-sheets", "--node", "node-1"},
"sheet +read": {"sheet", "+read", "--node", "node-1", "--sheet-id", "Sheet1"},
"minutes +detail": {"minutes", "+detail", "--ids", "u1,u2"},
"minutes +latest": {"minutes", "+latest", "--keyword", "fixture"},
"minutes +list-all": {"minutes", "+list-all", "--limit", "7"},
@@ -296,6 +343,20 @@ var paramAliasCandidateCompleteCommands = map[string][]string{
// that case the shared command template above cannot contain every canonical
// flag at once, so select a fixture-specific complete invocation here.
var paramAliasCompleteCommandVariants = map[string]map[string][]string{
"markdown create": {
"file": {"markdown", "create", "--file", "../../README.md", "--name", "fixture.md", "--space-id", "space-1"},
},
"markdown diff": {
"file": {"markdown", "diff", "--node", "node-1", "--file", "../../README.md", "--context", "3"},
},
"markdown overwrite": {
"file": {"markdown", "overwrite", "--node", "node-1", "--file", "../../README.md", "--name", "fixture.md", "--space-id", "space-1", "--yes"},
"dry-run": {"markdown", "overwrite", "--node", "node-1", "--content", "# Fixture", "--name", "fixture.md", "--space-id", "space-1", "--dry-run"},
},
"markdown patch": {
"dry-run": {"markdown", "patch", "--node", "node-1", "--pattern", "old", "--content", "new", "--regex", "--dry-run"},
},
"doc +copy": {
"folder": {"doc", "+copy", "--node", "node-1", "--folder", "folder-1", "--yes"},
"workspace": {"doc", "+copy", "--node", "node-1", "--workspace", "workspace-1", "--yes"},
@@ -969,6 +1030,121 @@ func assertParamAliasCannotBypassConfirmation(t *testing.T, aliasArgs []string)
}
}
// TestCrossPlatformCoverageEightProductTemplatedParamAliasesCannotBypassConfirmation
// exercises every distinct reviewed mutating complete-command template in the
// eight-product Shortcut expansion. The fixture gate already proves every
// alias resolves through PreParse; this gate removes the confirmation flag
// from one active alias invocation per distinct template and requires the
// runtime boundary to stop it before the first transport call. An explicit
// --dry-run is a reviewed preview path and must not carry a bypass flag.
func TestCrossPlatformCoverageEightProductTemplatedParamAliasesCannotBypassConfirmation(t *testing.T) {
concepts, err := cli.LoadParamConcepts()
if err != nil {
t.Fatalf("LoadParamConcepts() error = %v", err)
}
requiredTemplates := make(map[string]bool)
coveredTemplates := make(map[string]bool)
for _, fixture := range concepts.Fixture {
if strings.HasPrefix(fixture.Expect, "did-you-mean:") {
continue
}
product, _, _ := strings.Cut(fixture.Command, " ")
switch product {
case "attendance", "mail", "oa", "ding", "report", "sheet", "whiteboard", "markdown":
default:
continue
}
complete, ok := paramAliasCompleteCommand(fixture.Command, fixture.Expect)
if !ok {
continue
}
_, yesCount := removeExactArg(complete, "--yes")
_, userSayYesCount := removeExactArg(complete, "--user-say-yes")
confirmationCount := yesCount + userSayYesCount
confirmationArg := "--yes"
if userSayYesCount == 1 {
confirmationArg = "--user-say-yes"
}
_, dryRunCount := removeExactArg(complete, "--dry-run")
if dryRunCount > 1 {
t.Errorf("template must contain --dry-run at most once: command=%q args=%v", fixture.Command, complete)
continue
}
if meta, exists := cli.ResolveMeta(fixture.Command); exists {
switch meta.Safety.Confirmation {
case "user_required":
if dryRunCount == 1 {
if confirmationCount != 0 {
t.Errorf("Schema-confirmed dry-run template must not contain a confirmation bypass flag: command=%q args=%v", fixture.Command, complete)
}
continue
}
if confirmationCount != 1 {
t.Errorf("Schema-confirmed template must contain exactly one reviewed confirmation flag: command=%q confirmation=%q args=%v", fixture.Command, meta.Safety.Confirmation, complete)
continue
}
case "not_required":
if confirmationCount != 0 {
t.Errorf("Schema-unconfirmed template must not contain a confirmation bypass flag: command=%q confirmation=%q args=%v", fixture.Command, meta.Safety.Confirmation, complete)
continue
}
}
}
if confirmationCount == 0 {
continue
}
if confirmationCount != 1 {
t.Errorf("confirmation template must contain exactly one reviewed confirmation flag: command=%q args=%v", fixture.Command, complete)
continue
}
templateKey := fixture.Command + "\x00" + strings.Join(complete, "\x00")
requiredTemplates[templateKey] = true
if coveredTemplates[templateKey] {
continue
}
aliasArgs, replacements := replaceLongFlag(complete, fixture.Expect, fixture.Emitted)
if replacements != 1 {
t.Errorf("confirmation template for %q/%q must contain canonical --%s exactly once; replacements=%d args=%v", fixture.Command, fixture.Emitted, fixture.Expect, replacements, complete)
continue
}
coveredTemplates[templateKey] = true
t.Run(fixture.Command+"/"+fixture.Emitted, func(t *testing.T) {
assertTemplatedParamAliasCannotBypassConfirmation(t, fixture.Command, confirmationArg, aliasArgs)
})
}
if len(requiredTemplates) == 0 {
t.Fatal("reviewed complete-command templates contain no confirmation cases")
}
if len(coveredTemplates) != len(requiredTemplates) {
t.Fatalf("templated confirmation coverage = %d, want %d", len(coveredTemplates), len(requiredTemplates))
}
}
func assertTemplatedParamAliasCannotBypassConfirmation(t *testing.T, command, confirmationArg string, aliasArgs []string) {
t.Helper()
unconfirmedArgs, removals := removeExactArg(aliasArgs, confirmationArg)
if removals != 1 {
t.Fatalf("confirmation template must contain %s exactly once; removals=%d args=%v", confirmationArg, removals, aliasArgs)
}
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" {
if len(caller.calls) != 0 {
t.Fatalf("unconfirmed alias crossed the transport boundary before confirmation: args=%v calls=%#v", unconfirmedArgs, caller.calls)
}
return
}
t.Fatalf("unconfirmed alias command error = %#v, want confirmation_required\ncommand=%q args=%v calls=%#v", err, command, unconfirmedArgs, caller.calls)
}
func assertParamAliasFinalPayloadEquivalent(t *testing.T, command string, canonicalArgs, aliasArgs []string) {
t.Helper()
canonicalCaller := &paramAliasCaptureCaller{}
+911 -8
View File
@@ -1759,12 +1759,326 @@ var generatedParamAliases = []ParamAliasEntry{
},
Blocked: []string{"id", "task-id"},
},
{
CLIPath: "attendance +check-record",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"since": "start",
"staff-ids": "users",
"start-date": "start",
"start-time": "start",
"target-user-ids": "users",
"time-max": "end",
"time-min": "start",
"to": "end",
"until": "end",
"user-id-list": "users",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "date", "staff-id", "time", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +check-result",
Aliases: map[string]string{
"user-ids": "users",
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-result": "limit",
"max-results": "limit",
"max-time": "end",
"min-time": "start",
"page-size": "limit",
"per-page": "limit",
"since": "start",
"size": "limit",
"staff-ids": "users",
"start-date": "start",
"start-time": "start",
"take": "limit",
"target-user-ids": "users",
"time-max": "end",
"time-min": "start",
"to": "end",
"top": "limit",
"until": "end",
"user-id-list": "users",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "staff-id", "uid", "user", "user-id", "userid"},
Blocked: []string{"at-user-ids", "count", "cursor", "date", "page", "page-token", "staff-id", "time", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +get-adjustment-rule",
Aliases: map[string]string{
"adjustment-rule-id": "adjustment-id",
"rule-id": "adjustment-id",
},
Blocked: []string{"class-id", "group-id", "id", "overtime-id", "plan-id", "result-id"},
},
{
CLIPath: "attendance +get-approve-template",
Aliases: map[string]string{
"approval-type": "type",
"biz-type": "type",
},
Blocked: []string{"approval-types", "biz-types", "types"},
},
{
CLIPath: "attendance +get-checkin-record",
Aliases: map[string]string{
"begin": "start",
"corp-id": "operator-corp-id",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"operator-company-id": "operator-corp-id",
"operator-id": "operator-staff-id",
"operator-user-id": "operator-staff-id",
"since": "start",
"start-date": "start",
"start-time": "start",
"target-user-ids": "staff-ids",
"time-max": "end",
"time-min": "start",
"to": "end",
"until": "end",
"user-id-list": "staff-ids",
"user-ids": "staff-ids",
"users": "staff-ids",
},
Blocked: []string{"at-user-ids", "date", "name", "time", "to-user"},
Ambiguous: []string{"staff-id", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +get-leave-records",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"since": "start",
"staff-id": "user",
"start-date": "start",
"start-time": "start",
"time-max": "end",
"time-min": "start",
"to": "end",
"uid": "user",
"until": "end",
"user-id": "user",
"userid": "user",
"vacation-type-code": "leave-code",
},
Blocked: []string{"at-user-ids", "date", "id", "leave-name", "leave-names", "name", "rule-id", "time", "to-user", "user-ids", "users", "vacation-name"},
},
{
CLIPath: "attendance +get-overtime-rule",
Aliases: map[string]string{
"overtime-rule-id": "overtime-id",
"rule-id": "overtime-id",
},
Blocked: []string{"adjustment-id", "class-id", "group-id", "id", "plan-id", "result-id"},
},
{
CLIPath: "attendance +get-schedule",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"since": "start",
"staff-ids": "users",
"start-date": "start",
"start-time": "start",
"target-user-ids": "users",
"time-max": "end",
"time-min": "start",
"to": "end",
"until": "end",
"user-id-list": "users",
"user-ids": "users",
"work-date-begin": "start",
"work-date-end": "end",
},
Blocked: []string{"at-user-ids", "date", "staff-id", "time", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +get-self-setting",
Aliases: map[string]string{
"scene": "setting-scene",
"staff-id": "user",
"uid": "user",
"user-id": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "name", "setting-item", "setting-type", "to-user", "user-ids", "users"},
},
{
CLIPath: "attendance +get-summary",
Aliases: map[string]string{
"attendance-date": "date",
"period": "stats-type",
"staff-id": "user",
"statistics-type": "stats-type",
"summary-type": "stats-type",
"uid": "user",
"user-id": "user",
"userid": "user",
},
Blocked: []string{"at-user-ids", "end", "end-date", "name", "start", "start-date", "time", "to-user", "user-ids", "users"},
},
{
CLIPath: "attendance +list-approve",
Aliases: map[string]string{
"approval-types": "types",
"begin": "start",
"biz-types": "types",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"since": "start",
"staff-ids": "users",
"start-date": "start",
"start-time": "start",
"target-user-ids": "users",
"time-max": "end",
"time-min": "start",
"to": "end",
"until": "end",
"user-id-list": "users",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "date", "staff-id", "time", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +query-report-data",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"report-column-ids": "columns",
"since": "start",
"staff-ids": "users",
"start-date": "start",
"start-time": "start",
"target-user-ids": "users",
"time-max": "end",
"time-min": "start",
"to": "end",
"until": "end",
"user-id-list": "users",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "column-names", "date", "leave-names", "staff-id", "time", "uid", "user", "user-id", "userid"},
},
{
CLIPath: "attendance +search-adjustment-rule",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "attendance +search-class",
Aliases: map[string]string{
"class-filter-type": "filter-type",
"class-scope": "filter-type",
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "attendance +search-group",
Aliases: map[string]string{
"attendance-group-type": "type",
"current-page": "page",
"group-type": "type",
"include-ble": "query-ble",
"include-position": "query-position",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "attendance +search-overtime-rule",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "attendance check result",
@@ -3912,11 +4226,44 @@ var generatedParamAliases = []ParamAliasEntry{
},
Blocked: []string{"count", "cursor", "page-index", "page-token"},
},
{
CLIPath: "ding +list",
Aliases: map[string]string{
"message-type": "type",
"next-cursor": "cursor",
},
Blocked: []string{"next-token", "offset", "page", "page-token", "remind-type", "status"},
},
{
CLIPath: "ding +recall-personal",
Aliases: map[string]string{
"ding-id": "id",
"open-ding-id": "id",
},
Blocked: []string{"message-id", "request-id", "robot", "robot-code", "task-id", "uuid"},
},
{
CLIPath: "ding +receiver-status",
Aliases: map[string]string{
"id": "ding-id",
"id": "ding-id",
"open-ding-id": "ding-id",
},
Blocked: []string{"message-id", "request-id", "task-id", "uuid"},
},
{
CLIPath: "ding +send-personal",
Aliases: map[string]string{
"body": "content",
"idempotency-key": "uuid",
"message-content": "content",
"open-dingtalk-ids": "users",
"receiver-open-dingtalk-ids": "users",
"recipient-open-dingtalk-ids": "users",
"remind-type": "type",
"request-id": "uuid",
"text": "content",
},
Blocked: []string{"ding-id", "id", "message-type", "name", "open-ding-id", "open-dingtalk-id", "receiver-user-ids", "recipient-user-ids", "robot", "robot-code", "staff-id", "staff-ids", "status", "title", "user", "user-id", "user-ids"},
},
{
CLIPath: "ding message receiver-status",
@@ -6005,15 +6352,211 @@ var generatedParamAliases = []ParamAliasEntry{
},
Blocked: []string{"knowledge-base-id", "limit", "page-size", "part-size", "size", "space", "wiki-workspace-id", "workspace", "workspace-id"},
},
{
CLIPath: "mail +contact-list",
Aliases: map[string]string{
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "offset", "page"},
},
{
CLIPath: "mail +find-mail-user",
Aliases: map[string]string{
"keyword": "query",
"keywords": "query",
"q": "query",
"search-word": "query",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"name", "subject", "text", "title"},
Blocked: []string{"count", "name", "offset", "page", "subject", "text", "title"},
},
{
CLIPath: "mail +folder-list",
Aliases: map[string]string{
"folder-id": "folder",
},
Blocked: []string{"calendar-id", "name", "node-id"},
},
{
CLIPath: "mail +message",
Aliases: map[string]string{
"mail-id": "id",
"message-id": "id",
},
Blocked: []string{"attachment-id", "conversation-id", "internet-message-id", "thread-id"},
},
{
CLIPath: "mail +messages",
Aliases: map[string]string{
"mail-ids": "ids",
"message-ids": "ids",
},
Blocked: []string{"conversation-id", "conversation-ids", "mail-id", "message-id", "thread-id", "thread-ids"},
},
{
CLIPath: "mail +recent-mail",
Aliases: map[string]string{
"folder-id": "folder",
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"calendar-id", "count", "name", "node-id", "offset", "page"},
},
{
CLIPath: "mail +search-mail",
Aliases: map[string]string{
"limit": "size",
"max-result": "size",
"max-results": "size",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "size",
"page-token": "cursor",
"per-page": "size",
"take": "size",
"top": "size",
},
Blocked: []string{"count", "date", "folder", "folder-id", "from", "from-address", "has-attachments", "offset", "page", "sender", "subject", "to-address"},
},
{
CLIPath: "mail +template-list",
Aliases: map[string]string{
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "offset", "page"},
},
{
CLIPath: "mail +thread",
Aliases: map[string]string{
"conversation-id": "id",
"thread-id": "id",
},
Blocked: []string{"mail-id", "mail-ids", "message-id", "message-ids"},
},
{
CLIPath: "mail +thread-list",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"folder-id": "folder",
"from": "start",
"from-date": "start",
"max-result": "limit",
"max-results": "limit",
"max-time": "end",
"min-time": "start",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"since": "start",
"size": "limit",
"start-date": "start",
"start-time": "start",
"take": "limit",
"time-max": "end",
"time-min": "start",
"top": "limit",
},
Blocked: []string{"calendar-id", "count", "date", "name", "node-id", "offset", "page", "time"},
},
{
CLIPath: "mail +triage",
Aliases: map[string]string{
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "date", "folder", "folder-id", "from", "from-address", "has-attachments", "offset", "page", "sender", "subject", "to-address"},
},
{
CLIPath: "mail +unread-mail",
Aliases: map[string]string{
"limit": "size",
"max-result": "size",
"max-results": "size",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "size",
"page-token": "cursor",
"per-page": "size",
"take": "size",
"top": "size",
},
Blocked: []string{"count", "keyword", "offset", "page", "query", "sender", "subject"},
},
{
CLIPath: "mail +user-search",
Aliases: map[string]string{
"keywords": "keyword",
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"q": "keyword",
"query": "keyword",
"search-word": "keyword",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "name", "offset", "page", "subject", "text", "title"},
},
{
CLIPath: "mail folder update",
@@ -6052,6 +6595,123 @@ var generatedParamAliases = []ParamAliasEntry{
},
Blocked: []string{"name", "subject", "text", "title"},
},
{
CLIPath: "markdown create",
Aliases: map[string]string{
"body": "content",
"dingdrive-space-id": "space-id",
"drive-space-id": "space-id",
"file-name": "name",
"filename": "name",
"folder-id": "folder",
"knowledge-base-id": "workspace",
"local-file": "file",
"markdown-content": "content",
"plan-only": "dry-run",
"preview-only": "dry-run",
"source-file": "file",
"storage-space-id": "space-id",
"text": "content",
"wiki-workspace-id": "workspace",
},
Blocked: []string{"dentry-id", "doc-id", "document-id", "file-id", "node", "node-id", "output-path", "pattern", "regex", "replacement", "title", "url", "version", "version2"},
Ambiguous: []string{"content-file", "destination", "id", "path", "space"},
},
{
CLIPath: "markdown diff",
Aliases: map[string]string{
"base-version": "version",
"context-lines": "context",
"dentry-uuid": "node",
"diff-context": "context",
"from-version": "version",
"left-version": "version",
"local-file": "file",
"plan-only": "dry-run",
"preview-only": "dry-run",
"right-version": "version2",
"source-file": "file",
"surrounding-lines": "context",
"target-version": "version2",
"to-version": "version2",
"version-no": "version",
"version-number": "version",
},
Blocked: []string{"body", "content", "content-file", "folder", "folder-id", "name", "output-path", "pattern", "regex", "revision", "space-id", "version-id", "workspace", "workspace-id"},
Ambiguous: []string{"destination", "left", "path", "right"},
},
{
CLIPath: "markdown fetch",
Aliases: map[string]string{
"dentry-uuid": "node",
"destination-path": "output",
"dingdrive-space-id": "space-id",
"document-id": "node",
"download-to": "output",
"drive-space-id": "space-id",
"knowledge-base-id": "workspace",
"output-file": "output",
"output-path": "output",
"save-path": "output",
"save-to": "output",
"storage-space-id": "space-id",
"wiki-workspace-id": "workspace",
},
Blocked: []string{"content", "content-file", "dentry-id", "file", "file-path", "folder", "folder-id", "name", "parent-id", "pattern", "regex", "url", "version", "version2"},
Ambiguous: []string{"destination", "path", "space"},
},
{
CLIPath: "markdown overwrite",
Aliases: map[string]string{
"body": "content",
"dentry-uuid": "node",
"dingdrive-space-id": "space-id",
"document-id": "node",
"drive-space-id": "space-id",
"file-name": "name",
"filename": "name",
"knowledge-base-id": "workspace",
"local-file": "file",
"markdown-content": "content",
"no-write": "dry-run",
"preview-only": "dry-run",
"show-diff": "dry-run",
"source-file": "file",
"storage-space-id": "space-id",
"text": "content",
"wiki-workspace-id": "workspace",
},
Blocked: []string{"content-file", "dentry-id", "folder", "folder-id", "output-path", "parent-id", "pattern", "regex", "replacement", "title", "url", "version", "version2"},
Ambiguous: []string{"destination", "id", "path", "space"},
},
{
CLIPath: "markdown patch",
Aliases: map[string]string{
"dentry-uuid": "node",
"dingdrive-space-id": "space-id",
"document-id": "node",
"drive-space-id": "space-id",
"find-text": "pattern",
"knowledge-base-id": "workspace",
"match-pattern": "pattern",
"new-text": "content",
"no-write": "dry-run",
"old-text": "pattern",
"preview-only": "dry-run",
"regexp": "regex",
"regular-expression": "regex",
"replace-with": "content",
"replacement": "content",
"replacement-text": "content",
"search-pattern": "pattern",
"show-diff": "dry-run",
"storage-space-id": "space-id",
"use-regex": "regex",
"wiki-workspace-id": "workspace",
},
Blocked: []string{"content-file", "dentry-id", "file", "file-path", "folder", "folder-id", "name", "output-path", "parent-id", "query", "replacement-file", "url", "version", "version2"},
Ambiguous: []string{"body", "destination", "id", "path", "space", "text", "value"},
},
{
CLIPath: "minutes +action-items",
Aliases: map[string]string{
@@ -6655,6 +7315,128 @@ var generatedParamAliases = []ParamAliasEntry{
CLIPath: "minutes upload create",
Blocked: []string{"file", "file-path", "path", "template-ids"},
},
{
CLIPath: "oa +list-cc",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "oa +list-executed",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"page-no": "page",
"page-num": "page",
"q": "query",
"search-word": "query",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-size", "page-token", "subject", "text", "title"},
},
{
CLIPath: "oa +list-forms",
Aliases: map[string]string{
"max-result": "limit",
"max-results": "limit",
"next-cursor": "cursor",
"next-page-token": "cursor",
"next-token": "cursor",
"page-size": "limit",
"page-token": "cursor",
"per-page": "limit",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "offset", "page"},
},
{
CLIPath: "oa +list-pending",
Aliases: map[string]string{
"begin": "start",
"current-page": "page",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"max-time": "end",
"min-time": "start",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"since": "start",
"size": "limit",
"start-date": "start",
"start-time": "start",
"take": "limit",
"time-max": "end",
"time-min": "start",
"top": "limit",
},
Blocked: []string{"count", "cursor", "date", "name", "offset", "page-index", "page-token", "subject", "text", "time", "title"},
},
{
CLIPath: "oa +list-submitted",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "oa +my-initiated",
Aliases: map[string]string{
"current-page": "page",
"keyword": "query",
"keywords": "query",
"max-result": "limit",
"max-results": "limit",
"page-no": "page",
"page-num": "page",
"page-size": "limit",
"per-page": "limit",
"q": "query",
"search-word": "query",
"size": "limit",
"take": "limit",
"top": "limit",
},
Blocked: []string{"count", "cursor", "name", "page-index", "page-token", "subject", "text", "title"},
},
{
CLIPath: "oa +search-forms",
Aliases: map[string]string{
@@ -6675,6 +7457,100 @@ var generatedParamAliases = []ParamAliasEntry{
},
Blocked: []string{"name", "subject", "text", "title"},
},
{
CLIPath: "report +inbox-list",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"from-staff-ids": "sender-user-ids",
"limit": "size",
"max-result": "size",
"max-results": "size",
"max-time": "end",
"min-time": "start",
"page-size": "size",
"per-page": "size",
"sender-staff-ids": "sender-user-ids",
"since": "start",
"start-date": "start",
"start-time": "start",
"take": "size",
"time-max": "end",
"time-min": "start",
"top": "size",
},
Blocked: []string{"count", "date", "page", "sender-id", "sender-staff-id", "sender-user-id", "staff-id", "staff-ids", "time", "user-id", "user-ids"},
},
{
CLIPath: "report +outbox-list",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"limit": "size",
"max-result": "size",
"max-results": "size",
"max-time": "end",
"min-time": "start",
"modified-from": "modified-start",
"modified-to": "modified-end",
"name": "template-name",
"page-size": "size",
"per-page": "size",
"report-template-name": "template-name",
"since": "start",
"start-date": "start",
"start-time": "start",
"take": "size",
"time-max": "end",
"time-min": "start",
"top": "size",
"update-end": "modified-end",
"update-start": "modified-start",
"updated-end": "modified-end",
"updated-start": "modified-start",
},
Blocked: []string{"count", "date", "page", "report-template-id", "template-id", "template-type", "time"},
Ambiguous: []string{"template"},
},
{
CLIPath: "report +report-latest",
Aliases: map[string]string{
"begin": "start",
"end-date": "end",
"end-time": "end",
"from": "start",
"from-date": "start",
"max-time": "end",
"min-time": "start",
"name": "keyword",
"report-template-name": "keyword",
"since": "start",
"start-date": "start",
"start-time": "start",
"template-name": "keyword",
"time-max": "end",
"time-min": "start",
},
Blocked: []string{"date", "modified-end", "modified-start", "report-template-id", "template-id", "time"},
Ambiguous: []string{"template"},
},
{
CLIPath: "report +template-search",
Aliases: map[string]string{
"keyword": "query",
"keywords": "query",
"q": "query",
"search-word": "query",
},
Blocked: []string{"report-template-id", "subject", "template-id", "text", "title"},
Ambiguous: []string{"name", "template"},
},
{
CLIPath: "report list",
Aliases: map[string]string{
@@ -6693,6 +7569,33 @@ var generatedParamAliases = []ParamAliasEntry{
CLIPath: "report outbox list",
Blocked: []string{"template-type"},
},
{
CLIPath: "sheet +list-sheets",
Aliases: map[string]string{
"dentry-uuid": "node",
"doc": "node",
"doc-id": "node",
"document-id": "node",
"file-id": "node",
"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"},
},
{
CLIPath: "sheet +read",
Aliases: map[string]string{
"dentry-uuid": "node",
"doc": "node",
"doc-id": "node",
"document-id": "node",
"file-id": "node",
"node-id": "node",
"url": "node",
"worksheet-id": "sheet-id",
},
Blocked: []string{"a1-ranges", "block-id", "cell-ranges", "comment-id", "comment-key", "dentry-id", "destination-range", "folder", "folder-id", "id", "job-id", "name", "parent-id", "ranges", "revision", "role", "source-range", "source-sheet-id", "space-id", "start-cell", "target-range", "target-sheet-id", "task-id", "template-id", "title", "version", "workspace", "workspace-id"},
},
{
CLIPath: "todo +assign",
Aliases: map[string]string{
File diff suppressed because one or more lines are too long
@@ -10,6 +10,8 @@ import (
"runtime"
"sync"
"testing"
"github.com/spf13/cobra"
)
const (
@@ -39,7 +41,16 @@ func TestMain(m *testing.M) {
if os.Getenv(helpersShellStubEnv) == "1" {
os.Exit(runHelpersShellStub())
}
// Cobra's Windows pre-exec hook walks the process table on every Execute*
// call to detect Explorer launches. Helpers tests execute command trees
// thousands of times, and none of those in-process invocations can be an
// Explorer launch, so keep that production-only check out of the test
// process. This also prevents every new exhaustive harness from having to
// remember a test-local override.
originalMousetrapHelpText := cobra.MousetrapHelpText
cobra.MousetrapHelpText = ""
code := m.Run()
cobra.MousetrapHelpText = originalMousetrapHelpText
if helpersShellStubBaseDir != "" {
_ = os.RemoveAll(helpersShellStubBaseDir)
}
@@ -21,6 +21,7 @@ const (
CommandMigrationMove = "command_move"
CommandMigrationFlagExtraction = "flag_extraction"
CommandMigrationAvailability = "schema_availability_hardening"
)
// CommandMigrationManifest governs compatibility-preserving surface moves that
@@ -64,6 +65,12 @@ type CommandMigrationSchema struct {
SourceToolID string `json:"source_tool_id"`
ReplacementToolID string `json:"replacement_tool_id"`
Parameters []CommandParameterMigration `json:"parameters"`
Availability *CommandAvailabilityChange `json:"availability,omitempty"`
}
type CommandAvailabilityChange struct {
Before string `json:"before"`
After string `json:"after"`
}
type CommandParameterMigration struct {
@@ -126,14 +133,23 @@ func (m CommandMigrationManifest) Validate() error {
}
func (m CommandMigration) validate() error {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction && m.Kind != CommandMigrationAvailability {
return fmt.Errorf("invalid kind %q", m.Kind)
}
if !isExactCommandPath(m.Legacy.Command) || !isExactCommandPath(m.Replacement.Command) {
return fmt.Errorf("legacy and replacement must be exact command paths rooted at dws")
}
if m.Legacy.Command == m.Replacement.Command {
return fmt.Errorf("legacy and replacement command paths must differ")
if m.Kind == CommandMigrationAvailability {
if !isExactCommandPath(m.Legacy.Command) {
return fmt.Errorf("legacy must be an exact command path rooted at dws")
}
if m.Replacement != (CommandMigrationSide{}) {
return fmt.Errorf("schema_availability_hardening must not declare replacement")
}
} else {
if !isExactCommandPath(m.Legacy.Command) || !isExactCommandPath(m.Replacement.Command) {
return fmt.Errorf("legacy and replacement must be exact command paths rooted at dws")
}
if m.Legacy.Command == m.Replacement.Command {
return fmt.Errorf("legacy and replacement command paths must differ")
}
}
if strings.TrimSpace(m.Reason) == "" || m.Reason != strings.TrimSpace(m.Reason) {
return fmt.Errorf("migration must include a non-empty trimmed reason")
@@ -141,12 +157,15 @@ func (m CommandMigration) validate() error {
if m.State != CommandMigrationPending && m.State != CommandMigrationConsumed {
return fmt.Errorf("invalid state %q", m.State)
}
for label, state := range map[string]CommandMigrationState{
"legacy before": m.Legacy.Before,
"legacy after": m.Legacy.After,
"replacement before": m.Replacement.Before,
"replacement after": m.Replacement.After,
} {
states := map[string]CommandMigrationState{
"legacy before": m.Legacy.Before,
"legacy after": m.Legacy.After,
}
if m.Kind != CommandMigrationAvailability {
states["replacement before"] = m.Replacement.Before
states["replacement after"] = m.Replacement.After
}
for label, state := range states {
if err := state.validate(label); err != nil {
return err
}
@@ -155,7 +174,8 @@ func (m CommandMigration) validate() error {
!m.Legacy.After.Present || !m.Legacy.After.Runnable {
return fmt.Errorf("legacy command must remain runnable and start visible")
}
if m.Replacement.Before.Present || !m.Replacement.After.Present || !m.Replacement.After.Runnable || m.Replacement.After.Hidden {
if m.Kind != CommandMigrationAvailability &&
(m.Replacement.Before.Present || !m.Replacement.After.Present || !m.Replacement.After.Runnable || m.Replacement.After.Hidden) {
return fmt.Errorf("replacement command must migrate exactly from absent to visible runnable")
}
if err := m.Schema.validate(m.Kind); err != nil {
@@ -214,6 +234,13 @@ func (m CommandMigration) validate() error {
if m.LegacyFlag.Before.NoOpt != wantNoOpt || m.LegacyFlag.After.NoOpt != wantNoOpt {
return fmt.Errorf("flag_extraction legacy_flag no_opt must equal replacement constant %q", wantNoOpt)
}
case CommandMigrationAvailability:
if !m.Legacy.After.Hidden {
return fmt.Errorf("schema_availability_hardening legacy command must migrate exactly from visible to hidden")
}
if m.LegacyFlag != (CommandMigrationFlag{}) {
return fmt.Errorf("schema_availability_hardening must not declare legacy_flag")
}
}
return nil
}
@@ -259,9 +286,8 @@ func (f CommandMigrationFlag) validate() error {
func (s CommandMigrationSchema) validate(kind string) error {
for label, value := range map[string]string{
"product_id": s.ProductID,
"source_tool_id": s.SourceToolID,
"replacement_tool_id": s.ReplacementToolID,
"product_id": s.ProductID,
"source_tool_id": s.SourceToolID,
} {
if !isExactSchemaIdentifier(value) {
return fmt.Errorf("schema %s must be an exact identifier", label)
@@ -270,6 +296,21 @@ func (s CommandMigrationSchema) validate(kind string) error {
if s.Parameters == nil {
return fmt.Errorf("schema parameters must be an array")
}
if kind == CommandMigrationAvailability {
if s.ReplacementToolID != "" || len(s.Parameters) != 0 {
return fmt.Errorf("schema_availability_hardening must not declare a replacement tool or parameter mappings")
}
if s.Availability == nil || s.Availability.Before != "available" || s.Availability.After != "unavailable" {
return fmt.Errorf("schema_availability_hardening requires availability available to unavailable")
}
return nil
}
if !isExactSchemaIdentifier(s.ReplacementToolID) {
return fmt.Errorf("schema replacement_tool_id must be an exact identifier")
}
if s.Availability != nil {
return fmt.Errorf("%s must not declare schema availability", kind)
}
seenFrom := map[string]bool{}
seenTo := map[string]bool{}
seenConstantProperty := map[string]bool{}
@@ -344,6 +385,9 @@ func (m CommandMigration) key() string {
}
func (m CommandMigration) displayKey() string {
if m.Kind == CommandMigrationAvailability {
return fmt.Sprintf("%s %q", m.Kind, m.Legacy.Command)
}
return fmt.Sprintf("%s %q -> %q", m.Kind, m.Legacy.Command, m.Replacement.Command)
}
@@ -401,7 +445,7 @@ func AuthorizeCommandMigrations(
func validateCommandMoveLegacyLeaves(snapshot Snapshot, manifest CommandMigrationManifest) error {
commands := commandIndex(snapshot)
for _, migration := range manifest.Migrations {
if migration.Kind != CommandMigrationMove {
if migration.Kind != CommandMigrationMove && migration.Kind != CommandMigrationAvailability {
continue
}
if _, present := commands[migration.Legacy.Command]; !present {
@@ -409,7 +453,7 @@ func validateCommandMoveLegacyLeaves(snapshot Snapshot, manifest CommandMigratio
}
for _, command := range snapshot.Commands {
if strings.HasPrefix(command.Path, migration.Legacy.Command+" ") {
return fmt.Errorf("command_move legacy command %q must be a leaf", migration.Legacy.Command)
return fmt.Errorf("%s legacy command %q must be a leaf", migration.Kind, migration.Legacy.Command)
}
}
}
@@ -524,6 +568,15 @@ func sameCommandMigrationApproval(left, right CommandMigration) bool {
func matchCommandMigrationPhase(snapshot Snapshot, migration CommandMigration) commandMigrationPhase {
commands := commandIndex(snapshot)
legacy := commandMigrationStateForCommand(commands, migration.Legacy.Command)
if migration.Kind == CommandMigrationAvailability {
if legacy == migration.Legacy.Before {
return commandMigrationBefore
}
if legacy == migration.Legacy.After {
return commandMigrationAfter
}
return commandMigrationPartial
}
replacement := commandMigrationStateForCommand(commands, migration.Replacement.Command)
before := legacy == migration.Legacy.Before && replacement == migration.Replacement.Before
after := legacy == migration.Legacy.After && replacement == migration.Replacement.After
@@ -629,6 +682,10 @@ func commandMigrationAuthorizesChange(current, reference Snapshot, change Change
if change.Kind == "command_became_hidden" && change.Flag == "" {
return true
}
case CommandMigrationAvailability:
if change.Kind == "command_became_hidden" && change.Flag == "" {
return true
}
case CommandMigrationFlagExtraction:
if change.Kind == "flag_became_hidden" && change.Flag == migration.LegacyFlag.Name {
return true
@@ -186,6 +186,7 @@ func TestCrossPlatformCoverageCommandMigrationValidationEdges(t *testing.T) {
want string
}{
{"identifier", CommandMigrationMove, func(s *CommandMigrationSchema) { s.ProductID = "bad/id" }, "exact identifier"},
{"replacement identifier", CommandMigrationMove, func(s *CommandMigrationSchema) { s.ReplacementToolID = "bad/id" }, "replacement_tool_id"},
{"nil parameters", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters = nil }, "must be an array"},
{"bad from", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters[0].From = "bad name" }, "exact parameter"},
{"duplicate from", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters = append(s.Parameters, s.Parameters[0]) }, "duplicates from"},
@@ -241,6 +242,96 @@ func TestCrossPlatformCoverageCommandMigrationValidationEdges(t *testing.T) {
}
}
func TestCrossPlatformCoverageSchemaAvailabilityMigrationLifecycle(t *testing.T) {
pending := availabilityCommandMigrationManifest(CommandMigrationPending)
consumed := availabilityCommandMigrationManifest(CommandMigrationConsumed)
empty := CommandMigrationManifest{Version: CommandMigrationManifestVersion, Migrations: []CommandMigration{}}
before := testSnapshot(
testCommand("dws"),
testCommand("dws devapp"),
testCommand("dws devapp +event-list"),
)
after := testSnapshot(
testCommand("dws"),
testCommand("dws devapp"),
Command{Path: "dws devapp +event-list", Runnable: true, Hidden: true},
)
got, err := AuthorizeCommandMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil || len(got) != 1 {
t.Fatalf("availability lifecycle authorizations=%#v error=%v", got, err)
}
report, err := CompareAllWithInterfaceMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}},
FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("availability interface report compatible=%v error=%v report=%#v", report.Compatible, err, report)
}
if got, err := AuthorizeCommandMigrations(
before,
map[string]Snapshot{"merge-base": before, "stable": before},
empty,
pending,
); err != nil || len(got) != 0 {
t.Fatalf("candidate-added pending migration must remain inert: %#v, %v", got, err)
}
valid := pending.Migrations[0]
for _, test := range []struct {
name string
mutate func(*CommandMigration)
want string
}{
{"legacy path", func(m *CommandMigration) { m.Legacy.Command = "dws devapp *" }, "legacy must be an exact"},
{"replacement", func(m *CommandMigration) { m.Replacement.Command = "dws devapp other" }, "must not declare replacement"},
{"visible after", func(m *CommandMigration) { m.Legacy.After.Hidden = false }, "visible to hidden"},
{"legacy flag", func(m *CommandMigration) { m.LegacyFlag.Name = "legacy" }, "must not declare legacy_flag"},
{"missing availability", func(m *CommandMigration) { m.Schema.Availability = nil }, "requires availability"},
{"wrong availability", func(m *CommandMigration) { m.Schema.Availability.After = "available" }, "requires availability"},
{"parameter mapping", func(m *CommandMigration) {
m.Schema.Parameters = []CommandParameterMigration{{From: "cursor", To: "next-token"}}
}, "must not declare"},
} {
t.Run(test.name, func(t *testing.T) {
migration := cloneCommandMigration(valid)
if valid.Schema.Availability != nil {
change := *valid.Schema.Availability
migration.Schema.Availability = &change
}
test.mutate(&migration)
if err := migration.validate(); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("availability migration validation error=%v, want %q", err, test.want)
}
})
}
if got := valid.displayKey(); !strings.Contains(got, CommandMigrationAvailability) {
t.Fatalf("availability display key = %q", got)
}
if got := matchCommandMigrationPhase(testSnapshot(testCommand("dws")), valid); got != commandMigrationPartial {
t.Fatalf("missing availability command phase = %q", got)
}
parent := before
parent.Commands = append(append([]Command(nil), before.Commands...), testCommand("dws devapp +event-list detail"))
if err := validateCommandMoveLegacyLeaves(parent, pending); err == nil || !strings.Contains(err.Error(), "must be a leaf") {
t.Fatalf("availability parent command error = %v", err)
}
moveSchema := commandMigrationManifest(CommandMigrationPending).Migrations[0].Schema
moveSchema.Availability = &CommandAvailabilityChange{Before: "available", After: "unavailable"}
if err := moveSchema.validate(CommandMigrationMove); err == nil || !strings.Contains(err.Error(), "must not declare schema availability") {
t.Fatalf("ordinary move availability error = %v", err)
}
}
func TestCrossPlatformCoverageFlagExtractionRejectsRequiredLegacyFlag(t *testing.T) {
for _, test := range []struct {
name string
@@ -686,7 +777,10 @@ func flagExtractionCommandMigrationManifest(state string) CommandMigrationManife
func cloneCommandMigration(source CommandMigration) CommandMigration {
cloned := source
cloned.Schema.Parameters = append([]CommandParameterMigration(nil), source.Schema.Parameters...)
if source.Schema.Parameters != nil {
cloned.Schema.Parameters = make([]CommandParameterMigration, len(source.Schema.Parameters))
copy(cloned.Schema.Parameters, source.Schema.Parameters)
}
for index, parameter := range source.Schema.Parameters {
if parameter.ReplacementConstant == nil {
continue
@@ -703,6 +797,28 @@ func singleCommandMigrationManifest(state string) CommandMigrationManifest {
return manifest
}
func availabilityCommandMigrationManifest(state string) CommandMigrationManifest {
return CommandMigrationManifest{
Version: CommandMigrationManifestVersion,
Migrations: []CommandMigration{{
Kind: CommandMigrationAvailability,
Legacy: CommandMigrationSide{
Command: "dws devapp +event-list",
Before: CommandMigrationState{Present: true, Runnable: true},
After: CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Schema: CommandMigrationSchema{
ProductID: "devapp",
SourceToolID: "devapp.shortcut_event_list",
Parameters: []CommandParameterMigration{},
Availability: &CommandAvailabilityChange{Before: "available", After: "unavailable"},
},
State: state,
Reason: "Fail closed until terminal pagination evidence is available.",
}},
}
}
func modifiedCommandManifest(source CommandMigrationManifest) CommandMigrationManifest {
modified := source
modified.Migrations = append([]CommandMigration(nil), source.Migrations...)
+12 -8
View File
@@ -14,7 +14,7 @@ usage() {
# Single source of truth for the partition set. CI runs one job per partition and
# pins its shard names to this list, so a name that appears here without a
# dispatch entry below fails closed rather than silently skipping tests.
APP_PARTITIONS='schema a-b c-a-l c-m-r c-s-z c-other d-r s-z-example-fuzz'
APP_PARTITIONS='schema a-b c-a-l c-m-o c-p-r c-s-z c-other d-r s-z-example-fuzz'
mode="${1:-}"
partition=""
@@ -67,7 +67,8 @@ fi
schema_count=0
ab_count=0
cal_count=0
cmr_count=0
cmo_count=0
cpr_count=0
csz_count=0
cother_count=0
dr_count=0
@@ -79,7 +80,8 @@ while IFS= read -r test_name; do
Test*Schema*) schema_count=$((schema_count + 1)) ;;
Test[A-B]*) ab_count=$((ab_count + 1)) ;;
TestCrossPlatformCoverage[A-L]*) cal_count=$((cal_count + 1)) ;;
TestCrossPlatformCoverage[M-R]*) cmr_count=$((cmr_count + 1)) ;;
TestCrossPlatformCoverage[M-O]*) cmo_count=$((cmo_count + 1)) ;;
TestCrossPlatformCoverage[P-R]*) cpr_count=$((cpr_count + 1)) ;;
TestCrossPlatformCoverage[S-Z]*) csz_count=$((csz_count + 1)) ;;
TestC*) cother_count=$((cother_count + 1)) ;;
Test[D-R]*) dr_count=$((dr_count + 1)) ;;
@@ -103,7 +105,8 @@ for spec in \
"schema:$schema_count" \
"a-b:$ab_count" \
"c-a-l:$cal_count" \
"c-m-r:$cmr_count" \
"c-m-o:$cmo_count" \
"c-p-r:$cpr_count" \
"c-s-z:$csz_count" \
"c-other:$cother_count" \
"d-r:$dr_count" \
@@ -142,14 +145,14 @@ for name in $classified; do
done
total_count="$(wc -l < "$tests" | tr -d ' ')"
assigned_count=$((schema_count + ab_count + cal_count + cmr_count + csz_count + cother_count + dr_count + sz_count))
assigned_count=$((schema_count + ab_count + cal_count + cmo_count + cpr_count + csz_count + cother_count + dr_count + sz_count))
if [ "$assigned_count" -ne "$total_count" ]; then
printf 'app race partitions assigned %s tests, want %s\n' "$assigned_count" "$total_count" >&2
exit 1
fi
printf 'app race partitions cover %s top-level tests exactly once: schema=%s a-b=%s c-a-l=%s c-m-r=%s c-s-z=%s c-other=%s d-r=%s s-z-example-fuzz=%s\n' \
"$total_count" "$schema_count" "$ab_count" "$cal_count" "$cmr_count" "$csz_count" "$cother_count" "$dr_count" "$sz_count"
printf 'app race partitions cover %s top-level tests exactly once: schema=%s a-b=%s c-a-l=%s c-m-o=%s c-p-r=%s c-s-z=%s c-other=%s d-r=%s s-z-example-fuzz=%s\n' \
"$total_count" "$schema_count" "$ab_count" "$cal_count" "$cmo_count" "$cpr_count" "$csz_count" "$cother_count" "$dr_count" "$sz_count"
if [ "$mode" = "verify" ]; then
exit 0
@@ -208,7 +211,8 @@ run_named_partition() {
schema) run_partition schema no-race "$schema_pattern" ;;
a-b) run_partition a-b race '^Test[A-B]' "$schema_pattern" ;;
c-a-l) run_partition c-a-l race '^TestCrossPlatformCoverage[A-L]' "$schema_pattern" ;;
c-m-r) run_partition c-m-r race '^TestCrossPlatformCoverage[M-R]' "$schema_pattern" ;;
c-m-o) run_partition c-m-o race '^TestCrossPlatformCoverage[M-O]' "$schema_pattern" ;;
c-p-r) run_partition c-p-r race '^TestCrossPlatformCoverage[P-R]' "$schema_pattern" ;;
c-s-z) run_partition c-s-z race '^TestCrossPlatformCoverage[S-Z]' "$schema_pattern" ;;
c-other) run_partition c-other race '^TestC' '^Test.*Schema|^TestCrossPlatformCoverage' ;;
d-r) run_partition d-r race '^Test[D-R]' "$schema_pattern" ;;
@@ -13,6 +13,69 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
)
func TestCrossPlatformCoverageSchemaAvailabilityMigrationNormalizesOnlyAvailability(t *testing.T) {
baseline := baselineContract()
current := cloneContract(baseline)
mutateTool(&current, func(tool *toolSchema) { tool.Availability = "unavailable" })
migration := interfacesnapshot.CommandMigration{
Kind: interfacesnapshot.CommandMigrationAvailability,
Legacy: interfacesnapshot.CommandMigrationSide{
Command: "dws doc create",
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Schema: interfacesnapshot.CommandMigrationSchema{
ProductID: "doc",
SourceToolID: "doc.create",
Parameters: []interfacesnapshot.CommandParameterMigration{},
Availability: &interfacesnapshot.CommandAvailabilityChange{Before: "available", After: "unavailable"},
},
State: interfacesnapshot.CommandMigrationPending,
Reason: "Reviewed fail-closed availability hardening.",
}
normalized, err := normalizeSchemaCommandMigrations(baseline, current, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if got := normalized.Products["doc"].Tools["doc.create"].Availability; got != "unavailable" {
t.Fatalf("normalized availability = %q", got)
}
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
t.Fatalf("availability hardening failures = %v", failures)
}
unrelated := cloneContract(current)
mutateTool(&unrelated, func(tool *toolSchema) { tool.Risk = "high" })
normalized, err = normalizeSchemaCommandMigrations(baseline, unrelated, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, unrelated), "\n"); !strings.Contains(failures, "changed risk") {
t.Fatalf("unrelated risk drift was hidden: %q", failures)
}
alreadyAfter := cloneContract(current)
if _, err := normalizeSchemaCommandMigrations(alreadyAfter, current, []interfacesnapshot.CommandMigration{migration}); err != nil {
t.Fatalf("consumed merge-base availability should remain inert: %v", err)
}
wrongPath := cloneContract(current)
mutateTool(&wrongPath, func(tool *toolSchema) { tool.PrimaryCLIPath = "doc other" })
normalized, err = normalizeSchemaCommandMigrations(baseline, wrongPath, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if got := normalized.Products["doc"].Tools["doc.create"].Availability; got != "available" {
t.Fatalf("wrong-path migration changed availability to %q", got)
}
wrong := cloneContract(current)
mutateTool(&wrong, func(tool *toolSchema) { tool.Availability = "available" })
if _, err := normalizeSchemaCommandMigrations(baseline, wrong, []interfacesnapshot.CommandMigration{migration}); err == nil ||
!strings.Contains(err.Error(), "does not match Schema availability") {
t.Fatalf("wrong availability error = %v", err)
}
}
func TestCrossPlatformCoverageSchemaCommandMigrationsAuthorizeOnlyExactProjection(t *testing.T) {
baseline := schemaCommandMigrationContract(false)
current := schemaCommandMigrationContract(true)
+18
View File
@@ -1869,6 +1869,24 @@ func normalizeSchemaCommandMigrations(
normalizedProduct := normalized.Products[migration.Schema.ProductID]
normalizedTool := normalizedProduct.Tools[migration.Schema.SourceToolID]
switch migration.Kind {
case interfacesnapshot.CommandMigrationAvailability:
change := migration.Schema.Availability
if change != nil && oldTool.Availability == change.After && newSource.Availability == change.After {
continue
}
if change == nil || oldTool.Availability != change.Before || newSource.Availability != change.After {
return schemaContract{}, fmt.Errorf(
"approved availability hardening %q does not match Schema availability %q -> %q",
migration.Legacy.Command,
oldTool.Availability,
newSource.Availability,
)
}
if newSource.PrimaryCLIPath != legacyPath {
continue
}
normalizedTool.Availability = newSource.Availability
case interfacesnapshot.CommandMigrationMove:
if newSource.PrimaryCLIPath != replacementPath {
continue
+3
View File
@@ -1106,6 +1106,9 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
t.Fatal("Code Admission workflow missing Policy job boundaries")
}
policyJob := admission[policyStart:policyEnd]
if !strings.Contains(policyJob, "timeout-minutes: 15") {
t.Error("Policy job must retain enough headroom for full Schema policy validation")
}
requirePolicyEnv := func(step, nextStep string) {
t.Helper()
start := strings.Index(policyJob, " - name: "+step+"\n")