Files
dingtalk-workspace-cli/test/dev_functional/run_cases.py
T
qinzeandClaude Opus 4.8 2640338803 docs(dev): dingtalk-dev skill reference (2/2) + dev 功能测试
webapp 产品文档;dev 命令集功能测试用例与报告(测试产物 results.jsonl 不入库)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:11:21 +08:00

207 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""dev 命令树功能测试执行器:并发跑用例,输出 JSONL 结果。
只负责机械执行与取数(跑命令、收输出、比对预期子串);
失败归因与报告判断由调用方(agent)完成。
"""
import concurrent.futures
import json
import subprocess
import sys
import time
DWS = sys.argv[1] if len(sys.argv) > 1 else "/tmp/dws-dev"
PARALLEL = 20
TIMEOUT = 30
# 用例格式: (id, args, expect_exit, [output 必须包含的子串])
# expect_exit: 0=成功, 1=报错(CLI 校验/未知命令等)
U = "--unified-app-id"
CASES = [
# ---- 根与旧名 ----
("root-dev-help", ["dev", "--help"], 0, ["app", "connect", "doc"]),
("root-dev-app-help", ["dev", "app", "--help"], 0, ["credentials", "permission", "robot", "version"]),
("root-old-devapp-removed", ["devapp", "list"], 1, []),
("root-old-app-alias-removed", ["app", "list"], 1, []),
# ---- app list ----
("list-plain", ["dev", "app", "list", "--dry-run", "--format", "json"], 0, ["list_dev_app"]),
("list-name", ["dev", "app", "list", "--name", "Demo", "--dry-run", "--format", "json"], 0, ['"appName": "Demo"']),
("list-app-key", ["dev", "app", "list", "--app-key", "dingxxx", "--dry-run", "--format", "json"], 0, ['"appKey": "dingxxx"']),
("list-agent-id-removed", ["dev", "app", "list", "--agent-id", "123", "--dry-run", "--format", "json"], 1, ["unknown flag"]),
("list-sort", ["dev", "app", "list", "--creator", "张三", "--sort", "gmt_modified", "--order", "desc", "--dry-run", "--format", "json"], 0, ['"sortType"', '"sortOrder"']),
# ---- cursor 分页(纯透传:--cursor/--page-size 原样进 params,无合成/换算)----
("cursor-list-first", ["dev", "app", "list", "--page-size", "20", "--dry-run", "--format", "json"], 0, ['"pageSize": 20']),
("cursor-list-passthrough", ["dev", "app", "list", "--cursor", "TOKEN-ABC", "--page-size", "30", "--dry-run", "--format", "json"], 0, ['"cursor": "TOKEN-ABC"', '"pageSize": 30']),
# --page 不是独立 flag,但 cobra 把它当 --page-size 的前缀缩写(唯一前缀),故 --page 2 == --page-size 2
("cursor-list-page-abbrev", ["dev", "app", "list", "--page", "2", "--dry-run", "--format", "json"], 0, ['"pageSize": 2']),
("cursor-perm-passthrough", ["dev", "app", "permission", "list", "--unified-app-id", "u-1", "--cursor", "TOK-P", "--page-size", "50", "--dry-run", "--format", "json"], 0, ['"cursor": "TOK-P"', '"pageSize": 50']),
("cursor-perm-limit-removed", ["dev", "app", "permission", "list", "--unified-app-id", "u-1", "--limit", "10", "--dry-run", "--format", "json"], 1, ["unknown flag"]),
("cursor-version-passthrough", ["dev", "app", "version", "list", "--unified-app-id", "u-1", "--cursor", "TOK-V", "--page-size", "20", "--dry-run", "--format", "json"], 0, ['"cursor": "TOK-V"', '"pageSize": 20']),
("cursor-doc-passthrough", ["dev", "doc", "search", "--query", "x", "--cursor", "TOK-D", "--page-size", "5", "--dry-run", "--format", "json"], 0, ['"cursor": "TOK-D"', '"pageSize": 5']),
("cursor-doc-size-removed", ["dev", "doc", "search", "--query", "x", "--size", "5", "--dry-run", "--format", "json"], 1, ["unknown flag"]),
# ---- app get ----
("get-unified", ["dev", "app", "get", U, "u-1", "--dry-run", "--format", "json"], 0, ["get_dev_app", '"unifiedAppId": "u-1"']),
("get-agent-id-removed", ["dev", "app", "get", "--agent-id", "123", "--dry-run", "--format", "json"], 1, ["unknown flag"]),
("get-no-locator", ["dev", "app", "get", "--dry-run", "--format", "json"], 1, []),
# ---- app create ----
("create-full", ["dev", "app", "create", "--name", "Demo", "--desc", "d", "--icon", "m1", "--dry-run", "--format", "json"], 0, ["create_dev_app", '"appName": "Demo"', '"desc"', '"icon"']),
("create-no-name", ["dev", "app", "create", "--dry-run", "--format", "json"], 1, ["--name"]),
("create-type-removed", ["dev", "app", "create", "--name", "X", "--type", "internal", "--dry-run"], 1, ["unknown flag"]),
("create-write-guard", ["dev", "app", "create", "--name", "Demo", "--format", "json"], 1, []),
# ---- app update ----
("update-name", ["dev", "app", "update", U, "u-1", "--name", "N2", "--dry-run", "--format", "json"], 0, ["update_dev_app", '"appName": "N2"']),
("update-no-field", ["dev", "app", "update", U, "u-1", "--dry-run", "--format", "json"], 1, ["至少提供一项"]),
("update-no-locator", ["dev", "app", "update", "--name", "N2", "--dry-run", "--format", "json"], 1, []),
# ---- app lifecycle ----
("delete-dry", ["dev", "app", "delete", U, "u-1", "--dry-run", "--format", "json"], 0, ["delete_dev_app"]),
("disable-dry", ["dev", "app", "disable", U, "u-1", "--dry-run", "--format", "json"], 0, ["disable_dev_app"]),
("enable-dry", ["dev", "app", "enable", U, "u-1", "--dry-run", "--format", "json"], 0, ["enable_dev_app"]),
("delete-write-guard", ["dev", "app", "delete", U, "u-1", "--format", "json"], 1, []),
("delete-no-confirm-name", ["dev", "app", "delete", U, "u-1", "--yes", "--format", "json"], 1, ["二次确认"]),
("disable-no-locator", ["dev", "app", "disable", "--dry-run", "--format", "json"], 1, []),
# ---- credentials ----
("cred-unified", ["dev", "app", "credentials", "get", U, "u-1", "--dry-run", "--format", "json"], 0, ["get_dev_app_credentials"]),
("cred-agent", ["dev", "app", "credentials", "get", "--unified-app-id", "u-1", "--dry-run", "--format", "json"], 0, ['"unifiedAppId": "u-1"']),
("cred-no-locator", ["dev", "app", "credentials", "get", "--dry-run", "--format", "json"], 1, []),
# ---- webapp ----
("webapp-get", ["dev", "app", "webapp", "get", "--unified-app-id", "u-1", "--dry-run", "--format", "json"], 0, ["get_extension_webapp_config"]),
("webapp-config", ["dev", "app", "webapp", "config", "--unified-app-id", "u-1", "--homepage-url", "https://x.invalid/m", "--dry-run", "--format", "json"], 0, ["set_extension_webapp_config", '"homepageUrl"']),
("webapp-config-pc", ["dev", "app", "webapp", "config", U, "u-1", "--pc-homepage-url", "https://x.invalid/pc", "--omp-url", "https://x.invalid/o", "--dry-run", "--format", "json"], 0, ['"pcHomepageUrl"', '"ompUrl"']),
("webapp-config-no-field", ["dev", "app", "webapp", "config", "--unified-app-id", "u-1", "--dry-run", "--format", "json"], 1, []),
("webapp-config-guard", ["dev", "app", "webapp", "config", "--unified-app-id", "u-1", "--homepage-url", "https://x.invalid/m", "--format", "json"], 1, []),
# ---- permission list ----
("perm-list", ["dev", "app", "permission", "list", U, "u-1", "--dry-run", "--format", "json"], 0, ["list_dev_app_permissions", '"pageSize": 20', '"authStatus": "ALL"']),
("perm-list-page-size", ["dev", "app", "permission", "list", U, "u-1", "--page-size", "50", "--dry-run", "--format", "json"], 0, ['"pageSize": 50']),
("perm-list-keyword", ["dev", "app", "permission", "list", U, "u-1", "--keyword", "机器人", "--status", "unauthed", "--scope-type", "sns", "--dry-run", "--format", "json"], 0, ['"keyword"', '"authStatus": "UNAUTHED"', '"scopeType": "SNS"']),
("perm-list-scope", ["dev", "app", "permission", "list", U, "u-1", "--scope", "qyapi_robot_sendmsg", "--dry-run", "--format", "json"], 0, ['"scopeValue"']),
("perm-list-scope-alias", ["dev", "app", "permission", "list", U, "u-1", "--permission", "qyapi_robot_sendmsg", "--dry-run", "--format", "json"], 0, ['"scopeValue"']),
("perm-list-search-alias", ["dev", "app", "permission", "search", U, "u-1", "--dry-run", "--format", "json"], 0, ["list_dev_app_permissions"]),
("perm-list-no-locator", ["dev", "app", "permission", "list", "--dry-run", "--format", "json"], 1, []),
# ---- permission add ----
("perm-add-multi", ["dev", "app", "permission", "add", U, "u-1", "--permissions", "A,B", "--dry-run", "--format", "json"], 0, ["apply_dev_app_permissions", '"A"', '"B"']),
("perm-add-scope-alias", ["dev", "app", "permission", "add", U, "u-1", "--scope", "A", "--dry-run", "--format", "json"], 0, ['"scopeValues"']),
("perm-add-perm-alias", ["dev", "app", "permission", "add", U, "u-1", "--permission", "A", "--dry-run", "--format", "json"], 0, ['"scopeValues"']),
("perm-add-missing", ["dev", "app", "permission", "add", U, "u-1", "--dry-run", "--format", "json"], 1, ["--permissions"]),
("perm-add-guard", ["dev", "app", "permission", "add", U, "u-1", "--permissions", "A", "--format", "json"], 1, []),
# ---- permission remove ----
("perm-rm-single", ["dev", "app", "permission", "remove", U, "u-1", "--permissions", "A", "--dry-run", "--format", "json"], 0, ["remove_dev_app_permissions", '"scopeValue": "A"']),
("perm-rm-batch", ["dev", "app", "permission", "remove", U, "u-1", "--permissions", "A,B", "--dry-run", "--format", "json"], 0, ['"results"', '"scopeValue": "A"', '"scopeValue": "B"']),
("perm-rm-old-alias", ["dev", "app", "permission", "remove", U, "u-1", "--permission", "A", "--dry-run", "--format", "json"], 0, ['"scopeValue": "A"']),
("perm-rm-scope-alias", ["dev", "app", "permission", "remove", U, "u-1", "--scope", "A", "--dry-run", "--format", "json"], 0, ['"scopeValue": "A"']),
("perm-rm-missing", ["dev", "app", "permission", "remove", U, "u-1", "--dry-run", "--format", "json"], 1, ["--permissions"]),
("perm-rm-guard", ["dev", "app", "permission", "remove", U, "u-1", "--permissions", "A", "--format", "json"], 1, []),
# ---- member ----
("member-list", ["dev", "app", "member", "list", U, "u-1", "--dry-run", "--format", "json"], 0, ["list_dev_app_members"]),
("member-list-appid-removed", ["dev", "app", "member", "list", "--app-id", "u-1", "--dry-run", "--format", "json"], 1, ["unknown flag"]),
("member-list-missing", ["dev", "app", "member", "list", "--dry-run", "--format", "json"], 1, ["--unified-app-id"]),
("member-add", ["dev", "app", "member", "add", U, "u-1", "--users", "a,b", "--member-type", "DEVELOPER", "--dry-run", "--format", "json"], 0, ["add_dev_app_members", '"memberUserIds"']),
("member-add-trim", ["dev", "app", "member", "add", U, "u-1", "--users", " a , b ", "--member-type", "DEVELOPER", "--dry-run", "--format", "json"], 0, ['"a"', '"b"']),
("member-add-no-users", ["dev", "app", "member", "add", U, "u-1", "--member-type", "DEVELOPER", "--dry-run", "--format", "json"], 1, ["--users"]),
("member-remove", ["dev", "app", "member", "remove", U, "u-1", "--users", "a", "--member-type", "DEVELOPER", "--dry-run", "--format", "json"], 0, ["remove_dev_app_members"]),
("member-add-guard", ["dev", "app", "member", "add", U, "u-1", "--users", "a", "--member-type", "DEVELOPER", "--format", "json"], 1, []),
# ---- security ----
("sec-ip", ["dev", "app", "security", "config", U, "u-1", "--ip-whitelist", "192.0.2.1,192.0.2.2", "--dry-run", "--format", "json"], 0, ["update_dev_app_security_config", '"ipWhitelist"']),
("sec-redirect", ["dev", "app", "security", "config", U, "u-1", "--redirect-url", "https://x.invalid/cb", "--dry-run", "--format", "json"], 0, ['"redirectUrls"']),
("sec-sso", ["dev", "app", "security", "config", U, "u-1", "--sso-url", "https://x.invalid/sso", "--dry-run", "--format", "json"], 0, ['"ssoUrls"']),
("sec-combined", ["dev", "app", "security", "config", "--unified-app-id", "u-1", "--ip-whitelist", "192.0.2.1", "--redirect-url", "https://x.invalid/cb", "--dry-run", "--format", "json"], 0, ['"ipWhitelist"', '"redirectUrls"']),
("sec-no-field", ["dev", "app", "security", "config", U, "u-1", "--dry-run", "--format", "json"], 1, ["至少提供一项安全配置"]),
("sec-guard", ["dev", "app", "security", "config", U, "u-1", "--ip-whitelist", "192.0.2.1", "--format", "json"], 1, []),
# ---- robot ----
("robot-submit", ["dev", "app", "robot", "submit", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批", "--task-id", "t-1", "--dry-run", "--format", "json"], 0, ["submit_robot_create_task", '"taskId": "t-1"']),
("robot-result", ["dev", "app", "robot", "result", "--task-id", "t-1", "--format", "json", "--dry-run"], 0, ["query_robot_create_result"]),
("robot-result-missing", ["dev", "app", "robot", "result", "--format", "json", "--dry-run"], 1, []),
("robot-get", ["dev", "app", "robot", "get", U, "u-1", "--dry-run", "--format", "json"], 0, ["get_extension_robot_config"]),
("robot-config-upsert", ["dev", "app", "robot", "config", U, "u-1", "--name", "小助手", "--mode", "2", "--skills", "qa,approval", "--dry-run", "--format", "json"], 0, ["set_extension_robot_config", '"skillList"']),
# update 子命令已并入 config(upsert):robot update 落到 robot 组、报错重定向到 robot --help
("robot-update-removed", ["dev", "app", "robot", "update", U, "u-1", "--brief", "新简介", "--dry-run", "--format", "json"], 1, ["robot --help"]),
("robot-enable-pure", ["dev", "app", "robot", "enable", U, "u-1", "--dry-run", "--format", "json"], 0, ["enable_dev_app_robot"]),
("robot-config-no-field", ["dev", "app", "robot", "config", U, "u-1", "--dry-run", "--format", "json"], 1, []),
("robot-disable", ["dev", "app", "robot", "disable", U, "u-1", "--dry-run", "--format", "json"], 0, ["disable_dev_app_robot"]),
("robot-connect-removed", ["dev", "app", "robot", "connect", "--dry-run"], 1, []),
# ---- version ----
("ver-create", ["dev", "app", "version", "create", U, "u-1", "--version", "1.0.1", "--desc", "d", "--dry-run", "--format", "json"], 0, ["create_dev_app_version", '"version": "1.0.1"']),
("ver-list", ["dev", "app", "version", "list", U, "u-1", "--page-size", "20", "--dry-run", "--format", "json"], 0, ["list_dev_app_versions", '"pageSize": 20']),
("ver-get", ["dev", "app", "version", "get", U, "u-1", "--version-id", "v-1", "--dry-run", "--format", "json"], 0, ["get_dev_app_version_detail"]),
("ver-check", ["dev", "app", "version", "check-approval", U, "u-1", "--version-id", "v-1", "--dry-run", "--format", "json"], 0, ["publish_dev_app_version"]),
("ver-publish", ["dev", "app", "version", "publish", U, "u-1", "--version-id", "v-1", "--approver", "user1", "--confirm-sensitive", "--dry-run", "--format", "json"], 0, ["publish_dev_app_version", '"approverUserId"', '"confirmedSensitive"']),
("ver-publish-guard", ["dev", "app", "version", "publish", U, "u-1", "--version-id", "v-1", "--format", "json"], 1, []),
("ver-status", ["dev", "app", "version", "status", U, "u-1", "--version-id", "v-1", "--dry-run", "--format", "json"], 0, ["get_dev_app_version_status"]),
("event-list", ["dev", "app", "event", "list", U, "u-1", "--page-size", "10", "--dry-run", "--format", "json"], 0, ["list_dev_app_events", '"pageSize": 10']),
("event-sub", ["dev", "app", "event", "subscribe", U, "u-1", "--event-types", "t1,t2", "--callback-url", "https://x.invalid", "--dry-run", "--format", "json"], 0, ["subscribe_dev_app_event", '"eventTypes"']),
("event-unsub", ["dev", "app", "event", "unsubscribe", U, "u-1", "--event-types", "t1", "--dry-run", "--format", "json"], 0, ["unsubscribe_dev_app_event"]),
("event-list-missing", ["dev", "app", "event", "list", "--dry-run", "--format", "json"], 1, []),
("ver-create-missing", ["dev", "app", "version", "create", U, "u-1", "--dry-run", "--format", "json"], 1, []),
# ---- dev connect ----
("connect-dry-flags", ["dev", "connect", "--channel", "claudecode", "--robot-client-id", "id1", "--robot-client-secret", "s1", "--dry-run", "--format", "json"], 0, ['"channel": "claudecode"', '"cli"', '"connect"']),
("connect-dry-unified", ["dev", "connect", "--channel", "qoderwork", U, "ua-1", "--dry-run", "--format", "json"], 0, ['"credentialSource"', '"unifiedAppId": "ua-1"']),
("connect-bad-channel", ["dev", "connect", "--channel", "nope", "--robot-client-id", "i", "--robot-client-secret", "s", "--dry-run"], 1, ["未知渠道"]),
("connect-no-cred", ["dev", "connect", "--channel", "claudecode", "--dry-run", "--format", "json"], 1, ["--robot-client-id"]),
("connect-agent-opts", ["dev", "connect", "--channel", "codex", "--robot-client-id", "i", "--robot-client-secret", "s", "--agent-model", "m1", "--agent-workdir", "/tmp", "--agent-memory=false", "--reply-card=false", "--dry-run", "--format", "json"], 0, ['"channel": "codex"']),
# ---- dev doc ----
("doc-search", ["dev", "doc", "search", "--query", "errcode 40035", "--dry-run", "--format", "json"], 0, ["search_open_platform_docs", '"keyword": "errcode 40035"']),
("doc-search-positional", ["dev", "doc", "search", "回调失败", "--dry-run", "--format", "json"], 0, ['"keyword": "回调失败"']),
("doc-search-paging", ["dev", "doc", "search", "--query", "x", "--page-size", "5", "--dry-run", "--format", "json"], 0, ['"pageSize": 5']),
("doc-search-missing", ["dev", "doc", "search", "--dry-run", "--format", "json"], 1, ["--query"]),
# ---- pretty 出参 ----
("pretty-no-crash", ["dev", "app", "get", U, "u-1", "--dry-run", "--format", "pretty"], 0, []),
]
def run_case(case):
cid, args, want_exit, want_subs = case
started = time.time()
try:
proc = subprocess.run(
[DWS, *args], capture_output=True, text=True, timeout=TIMEOUT
)
out = (proc.stdout or "") + (proc.stderr or "")
exit_ok = (proc.returncode == 0) == (want_exit == 0)
missing = [s for s in want_subs if s not in out]
status = "PASS" if exit_ok and not missing else "FAIL"
return {
"id": cid, "status": status, "exit": proc.returncode,
"want_exit": want_exit, "missing": missing,
"ms": int((time.time() - started) * 1000),
"cmd": "dws " + " ".join(args),
"output_head": out[:600],
}
except subprocess.TimeoutExpired:
return {"id": cid, "status": "TIMEOUT", "ms": TIMEOUT * 1000,
"cmd": "dws " + " ".join(args), "output_head": ""}
def main():
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=PARALLEL) as pool:
for res in pool.map(run_case, CASES):
results.append(res)
print(f"[{res['status']}] {res['id']}", file=sys.stderr)
with open(sys.argv[2] if len(sys.argv) > 2 else "results.jsonl", "w") as f:
for r in results:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
passed = sum(1 for r in results if r["status"] == "PASS")
print(f"\n{passed}/{len(results)} PASS", file=sys.stderr)
if __name__ == "__main__":
main()