Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd40ea19a | ||
|
|
2f0f32f56f | ||
|
|
068d9ff2f5 | ||
|
|
418928b9a5 | ||
|
|
b047b2c3c9 | ||
|
|
a516e5f54a | ||
|
|
70e4e75c66 | ||
|
|
1116916b24 | ||
|
|
c0c81b4d70 | ||
|
|
c14e24569c | ||
|
|
8add2c00cf | ||
|
|
e15a2c4efb |
@@ -358,25 +358,6 @@ jobs:
|
||||
latestByName.set(run.name, run);
|
||||
}
|
||||
}
|
||||
// Also check commit statuses — some contexts (e.g. "AI Behavior")
|
||||
// are reported as commit statuses rather than check runs.
|
||||
const statuses = await github.paginate(
|
||||
github.rest.repos.listCommitStatusesForRef,
|
||||
{ owner, repo, ref: sha, per_page: 100 },
|
||||
);
|
||||
const seenStatusContexts = new Set();
|
||||
for (const status of statuses) {
|
||||
if (!requiredContexts.includes(status.context) || seenStatusContexts.has(status.context)) {
|
||||
continue;
|
||||
}
|
||||
seenStatusContexts.add(status.context);
|
||||
if (!latestByName.has(status.context)) {
|
||||
latestByName.set(status.context, {
|
||||
conclusion: status.state,
|
||||
status: "completed",
|
||||
});
|
||||
}
|
||||
}
|
||||
const missing = requiredContexts.filter((name) => !latestByName.has(name));
|
||||
const nonSuccess = requiredContexts.flatMap((name) => {
|
||||
const run = latestByName.get(name);
|
||||
@@ -772,25 +753,6 @@ jobs:
|
||||
latestByName.set(run.name, run);
|
||||
}
|
||||
}
|
||||
// Also check commit statuses — some contexts (e.g. "AI Behavior")
|
||||
// are reported as commit statuses rather than check runs.
|
||||
const statuses = await github.paginate(
|
||||
github.rest.repos.listCommitStatusesForRef,
|
||||
{ owner, repo, ref: sha, per_page: 100 },
|
||||
);
|
||||
const seenStatusContexts = new Set();
|
||||
for (const status of statuses) {
|
||||
if (!requiredContexts.includes(status.context) || seenStatusContexts.has(status.context)) {
|
||||
continue;
|
||||
}
|
||||
seenStatusContexts.add(status.context);
|
||||
if (!latestByName.has(status.context)) {
|
||||
latestByName.set(status.context, {
|
||||
conclusion: status.state,
|
||||
status: "completed",
|
||||
});
|
||||
}
|
||||
}
|
||||
const missing = requiredContexts.filter((name) => !latestByName.has(name));
|
||||
const nonSuccess = requiredContexts.flatMap((name) => {
|
||||
const run = latestByName.get(name);
|
||||
|
||||
@@ -54,3 +54,11 @@ test/dev_functional/results.jsonl
|
||||
/coverage-policy.txt
|
||||
/coverage.html
|
||||
dwsbin
|
||||
|
||||
# Local shortcut eval / real-backend capture artifacts — may contain real PII
|
||||
# (employee names/emails, userIds, conversation & message IDs). Never commit.
|
||||
/docs/shortcut-real-read-results.json
|
||||
/docs/shortcut-real-write-results.json
|
||||
/docs/shortcut-comparison.html
|
||||
/docs/shortcut-gsb-eval.*
|
||||
/scripts/run_shortcut_real_read_matrix.py
|
||||
|
||||
+3
-5
@@ -6,13 +6,9 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Relaxed stable promotion contract** — a stable release still requires a delivered, non-withdrawn beta baseline in its commit history, but no longer requires a byte-identical tree with that beta; reviewed commits merged to `main` after the beta can now ship in the stable release. Local releases now accept any sealed commit contained in `main` history and push only the release tag, so `main` is never frozen during the beta-to-stable window.
|
||||
|
||||
## [1.0.53] - 2026-07-21
|
||||
|
||||
This release promotes the sealed `v1.0.53-beta.7` contents to stable. It adds enterprise onboarding, declarative shortcuts, Sheet/Aitable writes, multi-account profiles, and broader personal IM events, while hardening authentication and the guarded release path.
|
||||
This release promotes the validated `v1.0.53-beta.7` baseline to stable. It adds enterprise onboarding, declarative shortcuts, Sheet/Aitable writes, multi-account profiles, and broader personal IM events, while hardening authentication and the guarded release path.
|
||||
|
||||
### Added
|
||||
|
||||
@@ -25,12 +21,14 @@ This release promotes the sealed `v1.0.53-beta.7` contents to stable. It adds en
|
||||
|
||||
- **Personal event output contract** (#651) — `event consume` now emits event-specific top-level structured fields; scripts that consumed the former transport envelope must use the flat fields or select `-f raw`, while `--debug-raw-events` retains the diagnostic envelope.
|
||||
- **Guarded release lifecycle** — beta/stable publication now uses explicit promotion, immutable delivery proofs, protected recovery, and tag-bound optional OSS policy; an unprovisioned OSS mirror is sealed as `deferred` so GitHub, npm, and Homebrew are not blocked.
|
||||
- **Relaxed stable promotion contract** (#729) — a stable release still requires a delivered, non-withdrawn beta baseline in its commit history, but no longer requires a byte-identical tree with that beta; reviewed commits merged to `main` after the beta can now ship in the stable release. Local releases now accept any sealed commit contained in `main` history and push only the release tag, so `main` is never frozen during the beta-to-stable window.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Authentication and credential reliability** — organization-policy denials stop before mutation or polling, long-running clients reload and refresh access tokens consistently, concurrent credential writes are atomic, and Windows portable-auth commands fail before reading or writing unsupported credential bundles.
|
||||
- **Command validation and compatibility** — invalid Sheet/task targets fail locally, IM shortcuts preserve AI-tag and alias compatibility, and Aitable import uploads require and forward a positive file size.
|
||||
- **Release publication reliability** — GitHub draft publication is bound to one verified release ID and exact assets, preflight uses isolated installer worktrees, guarded local tags remain compatible, cloud planning fingerprints the actual allocated release refs, and npm channel verification waits for bounded registry propagation without moving tags.
|
||||
- **Package-manager version verification** (#735) — npm-vendored, Homebrew-installed, and packaged release binaries are now verified by searching their raw bytes for the injected version marker, so a correctly versioned stable binary is no longer rejected when the short version marker coalesces with adjacent printable linker metadata; incorrect or missing markers still fail closed.
|
||||
|
||||
## [1.0.53-beta.7] - 2026-07-21
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -53,8 +53,6 @@ while [ "$#" -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
[ -z "$EXPECTED_VERSION" ] || need_cmd strings
|
||||
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-package-verify-XXXXXX")"
|
||||
HOME_AGENT_PARENTS="
|
||||
.claude
|
||||
@@ -145,8 +143,8 @@ verify_npm() {
|
||||
if [ -n "$EXPECTED_VERSION" ]; then
|
||||
vendor_bin="$npm_prefix/lib/node_modules/dingtalk-workspace-cli/vendor/dws"
|
||||
need_file "$vendor_bin"
|
||||
strings "$vendor_bin" | grep -Fqx "v$EXPECTED_VERSION" || \
|
||||
err "npm-installed binary does not embed expected version v$EXPECTED_VERSION"
|
||||
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$vendor_bin" || \
|
||||
err "npm-installed binary does not contain expected version marker v$EXPECTED_VERSION"
|
||||
EXPECTED_VERSION="$EXPECTED_VERSION" node -e '
|
||||
const pkg = require(process.argv[1]);
|
||||
if (pkg.version !== process.env.EXPECTED_VERSION) process.exit(1);
|
||||
@@ -193,8 +191,8 @@ verify_brew() {
|
||||
[ -x "$prefix/bin/dws" ] || err "brew install did not create $prefix/bin/dws"
|
||||
"$prefix/bin/dws" --help >/dev/null
|
||||
if [ -n "$EXPECTED_VERSION" ]; then
|
||||
strings "$prefix/bin/dws" | grep -Fqx "v$EXPECTED_VERSION" || \
|
||||
err "Homebrew-installed binary does not embed expected version v$EXPECTED_VERSION"
|
||||
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$prefix/bin/dws" || \
|
||||
err "Homebrew-installed binary does not contain expected version marker v$EXPECTED_VERSION"
|
||||
fi
|
||||
need_file "$prefix/share/dingtalk-workspace-cli-local/skills/dws/SKILL.md"
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ verify_binary_version() {
|
||||
printf '%s does not contain the expected dws binary\n' "$asset" >&2
|
||||
return 1
|
||||
}
|
||||
strings "$binary" | grep -Fqx "v$SEMVER" || {
|
||||
LC_ALL=C grep -aFq "v$SEMVER" "$binary" || {
|
||||
printf '%s binary does not embed expected version v%s\n' "$asset" "$SEMVER" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run every read shortcut against the real DWS backend.
|
||||
|
||||
This launches the built CLI once per read shortcut. It does not use --mock and
|
||||
does not use --dry-run. Inputs are synthetic but realistic where a shortcut
|
||||
expects a name, date, or query; resource identifiers remain test placeholders
|
||||
when no resource has been created for that command.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from shortcut_real_result import (
|
||||
classify_failure,
|
||||
sanitize_result,
|
||||
summarize_failure_categories,
|
||||
classify_real_status,
|
||||
summarize_results,
|
||||
)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MATRIX_PATH = Path("/private/tmp/dws-shortcut-matrix.json")
|
||||
OUT_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
|
||||
BIN = os.environ.get("DWS_REAL_TEST_BIN", "/private/tmp/dws-real-test")
|
||||
DEVAPP_FIXTURE_ID = "678f27ec-4339-49d8-9c49-371b284bf552"
|
||||
DEVAPP_FIXTURE_VERSION_ID = "4743accb-45e2-4bc9-8c96-74fc62ace2e8"
|
||||
CHAT_FIXTURE_OPEN_CONVERSATION_ID = "cid3Jijzhe2aqs9ysOXjhi05g=="
|
||||
CHAT_FIXTURE_GROUP_NAME = "浅曦-kida,Dennis,秋画"
|
||||
CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID = "cidie1367hAfBxqipzE59k5sknHLrHmvYkw98NADhfnjPI="
|
||||
CHAT_FIXTURE_OPEN_MESSAGE_ID = "msgEuOor1PmFBNlx9M06N9z1Q=="
|
||||
CHAT_FIXTURE_OPEN_TASK_ID = "y/wM6Lo+9GbIqtILYPv1BZDcMW+2FgnqskgcpdOiMdM="
|
||||
CALENDAR_FIXTURE_EVENT_ID = "THN4YUtOTlplYU9sZzd2czE4YURLQT09_1784078100000"
|
||||
DOC_FIXTURE_NODE_ID = "P0MALyR8knpgo9GycY7ZlMxlJ3bzYmDO"
|
||||
DOC_FIXTURE_FOLDER_ID = "Amq4vjg89ZOAdqyaSMKpApXdW3kdP0wQ"
|
||||
DOC_FIXTURE_EXPORT_JOB_ID = "29346731713"
|
||||
SHEET_FIXTURE_NODE_ID = "mweZ92PV6O36dZbnsMZx70ylJxEKBD6p"
|
||||
DING_FIXTURE_OPEN_DING_ID = "5D73E3AC29C780072D1CD56C6874ACC2"
|
||||
CONTACT_FIXTURE_MOBILE = "13161187007"
|
||||
AITABLE_FIXTURE_BASE_ID = "gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk"
|
||||
AITABLE_FIXTURE_TABLE_ID = "hERWDMS"
|
||||
AITABLE_FIXTURE_VIEW_ID = "qvGDAH2"
|
||||
AITABLE_FIXTURE_FORM_VIEW_ID = "lmeV1cb"
|
||||
AITABLE_FIXTURE_FIELD_ID = "01ZM8y7"
|
||||
AITABLE_FIXTURE_RECORD_ID = "1015oH3OXy"
|
||||
AITABLE_FIXTURE_DASHBOARD_ID = "KY9tlWg5NEHgfs8WT6IO2"
|
||||
AITABLE_FIXTURE_CHART_ID = "widget-dlxFo0tNSImp4ITpDn5fQ"
|
||||
|
||||
HELD_CASES = {
|
||||
("devapp", "+credentials-get"):
|
||||
"该命令会读取真实应用凭证/密钥;不能用真实 app 自动执行。当前仅用占位 ID 验证负向路径,真实成功需人工在安全环境单独确认。",
|
||||
}
|
||||
|
||||
|
||||
def ensure_matrix() -> dict:
|
||||
env = os.environ.copy()
|
||||
env.setdefault("GOCACHE", "/private/tmp/dws_gocache")
|
||||
raw = subprocess.check_output(
|
||||
["go", "run", "./scripts/gen_shortcut_test_matrix.go"],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
)
|
||||
MATRIX_PATH.write_text(raw, encoding="utf-8")
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def replace_flag_values(args: list[str], service: str, command: str) -> list[str]:
|
||||
today = dt.date.today()
|
||||
start = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
|
||||
end = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=11, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
|
||||
no_id = "DWSREALREADNOSUCHID0000000000000"
|
||||
no_conv = "cidDWSREALREADNOSUCHCONV"
|
||||
day = str(today)
|
||||
yesterday = str(today - dt.timedelta(days=1))
|
||||
datetime_start = dt.datetime.now().replace(hour=10, minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
datetime_end = (dt.datetime.now() + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
replacements = {
|
||||
"name": "DWS shortcut 真实测试",
|
||||
"query": "测试",
|
||||
"keyword": "测试",
|
||||
"q": "测试",
|
||||
"text": "测试",
|
||||
"title": "测试",
|
||||
"phone": "13000000000",
|
||||
"mobile": "13000000000",
|
||||
"user": "冬翔",
|
||||
"users": "冬翔",
|
||||
"to": "冬翔",
|
||||
"with": "冬翔",
|
||||
"who": "冬翔",
|
||||
"dept": "模型算法",
|
||||
"dept-id": "842379556",
|
||||
"department-id": "842379556",
|
||||
"start": start,
|
||||
"end": end,
|
||||
"from": str(today - dt.timedelta(days=7)),
|
||||
"to-date": str(today),
|
||||
"date": str(today),
|
||||
"time": datetime_start,
|
||||
"days": "7",
|
||||
"types": "leave",
|
||||
"columns": "1001",
|
||||
"limit": "10",
|
||||
"page": "1",
|
||||
"page-size": "10",
|
||||
"cursor": "0",
|
||||
"calendar-id": "primary",
|
||||
"event": no_id,
|
||||
"type": "ALL",
|
||||
"types": "leave",
|
||||
"columns": "1001",
|
||||
"role-types": "executor",
|
||||
"status": "false",
|
||||
"artifacts": "basic",
|
||||
"direction": "older",
|
||||
"file-types": "alidoc",
|
||||
"order-by": "name",
|
||||
"order": "asc",
|
||||
"space-id": "1",
|
||||
"space": "测试",
|
||||
"base": no_id,
|
||||
"base-id": no_id,
|
||||
"table": no_id,
|
||||
"table-id": no_id,
|
||||
"view-id": no_id,
|
||||
"record-id": no_id,
|
||||
"record-ids": no_id,
|
||||
"field-id": no_id,
|
||||
"dashboard-id": no_id,
|
||||
"chart-id": no_id,
|
||||
"workflow-id": no_id,
|
||||
"node": no_id,
|
||||
"doc": no_id,
|
||||
"folder": no_id,
|
||||
"workspace": no_id,
|
||||
"group": no_conv,
|
||||
"conversation-id": no_conv,
|
||||
"open-conversation-id": no_conv,
|
||||
"message-id": no_id,
|
||||
"msg-id": no_id,
|
||||
"id": no_id,
|
||||
"session-id": no_id,
|
||||
"process-instance-id": no_id,
|
||||
"task-id": no_id,
|
||||
"template-id": no_id,
|
||||
"mail-id": no_id,
|
||||
"filters": "{}",
|
||||
"sort": "[]",
|
||||
}
|
||||
if service == "calendar" and command == "+free-slots":
|
||||
replacements["from"] = "9"
|
||||
replacements["to"] = "18"
|
||||
if service == "calendar":
|
||||
replacements["event"] = CALENDAR_FIXTURE_EVENT_ID
|
||||
replacements["cursor"] = ""
|
||||
if command == "+freebusy":
|
||||
replacements["users"] = "103262"
|
||||
if service == "doc" and command == "+comment-list":
|
||||
replacements["type"] = "global"
|
||||
if service == "doc":
|
||||
replacements["node"] = DOC_FIXTURE_NODE_ID
|
||||
replacements["doc"] = DOC_FIXTURE_NODE_ID
|
||||
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
|
||||
replacements["job-id"] = DOC_FIXTURE_EXPORT_JOB_ID
|
||||
if service == "drive":
|
||||
replacements["node"] = DOC_FIXTURE_NODE_ID
|
||||
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
|
||||
if service == "todo":
|
||||
replacements["task-id"] = "55119034912"
|
||||
replacements["size"] = "10"
|
||||
if service == "aitable":
|
||||
replacements["name"] = "Real共创版设备去向登记"
|
||||
replacements["base"] = AITABLE_FIXTURE_BASE_ID
|
||||
replacements["base-id"] = AITABLE_FIXTURE_BASE_ID
|
||||
replacements["table"] = AITABLE_FIXTURE_TABLE_ID
|
||||
replacements["table-id"] = AITABLE_FIXTURE_TABLE_ID
|
||||
replacements["view-id"] = AITABLE_FIXTURE_VIEW_ID
|
||||
replacements["view-ids"] = AITABLE_FIXTURE_VIEW_ID
|
||||
replacements["field-id"] = AITABLE_FIXTURE_FIELD_ID
|
||||
replacements["field-ids"] = AITABLE_FIXTURE_FIELD_ID
|
||||
replacements["record-id"] = AITABLE_FIXTURE_RECORD_ID
|
||||
replacements["record-ids"] = AITABLE_FIXTURE_RECORD_ID
|
||||
replacements["dashboard-id"] = AITABLE_FIXTURE_DASHBOARD_ID
|
||||
replacements["chart-id"] = AITABLE_FIXTURE_CHART_ID
|
||||
if command.startswith("+form-"):
|
||||
replacements["view-id"] = AITABLE_FIXTURE_FORM_VIEW_ID
|
||||
if command == "+resolve-table":
|
||||
replacements["name"] = "Mac Mini"
|
||||
if service == "ding" and command == "+list":
|
||||
replacements["type"] = "ALL"
|
||||
if service == "ding" and command == "+receiver-status":
|
||||
replacements["ding-id"] = DING_FIXTURE_OPEN_DING_ID
|
||||
if service == "contact" and command == "+list-sub-depts":
|
||||
replacements["dept"] = "842379556"
|
||||
if service == "contact":
|
||||
replacements["name"] = "冬翔"
|
||||
if command == "+by-mobile":
|
||||
replacements["mobile"] = CONTACT_FIXTURE_MOBILE
|
||||
if command == "+resolve-dept":
|
||||
replacements["name"] = "模型算法"
|
||||
if service == "oa":
|
||||
now_ms = int(dt.datetime.now().timestamp() * 1000)
|
||||
replacements["start"] = str(now_ms - 7 * 24 * 60 * 60 * 1000)
|
||||
replacements["end"] = str(now_ms)
|
||||
replacements["page"] = "1"
|
||||
replacements["limit"] = "10"
|
||||
if service == "report":
|
||||
replacements["start"] = (dt.datetime.now() - dt.timedelta(days=7)).replace(microsecond=0).isoformat() + "+08:00"
|
||||
replacements["end"] = dt.datetime.now().replace(microsecond=0).isoformat() + "+08:00"
|
||||
replacements["modified-start"] = replacements["start"]
|
||||
replacements["modified-end"] = replacements["end"]
|
||||
if service == "attendance":
|
||||
replacements.update({
|
||||
"user": "202397",
|
||||
"users": "202397",
|
||||
"staff-ids": "202397",
|
||||
"operator-staff-id": "202397",
|
||||
"leave-code": "731ed089-62ff-4734-a6c7-3c8fcc8294fc",
|
||||
"leave-names": "年假",
|
||||
"start": yesterday,
|
||||
"end": day,
|
||||
"from": yesterday,
|
||||
"to-date": day,
|
||||
"date": day,
|
||||
})
|
||||
if command in {"+get-checkin-record", "+query-report-data", "+query-report-leave"}:
|
||||
replacements["start"] = datetime_start
|
||||
replacements["end"] = datetime_end
|
||||
if command == "+get-approve-template":
|
||||
replacements["type"] = "leave"
|
||||
if command == "+search-group":
|
||||
replacements["type"] = "FIXED"
|
||||
if service == "devapp":
|
||||
replacements["unified-app-id"] = DEVAPP_FIXTURE_ID
|
||||
replacements["version-id"] = DEVAPP_FIXTURE_VERSION_ID
|
||||
replacements["cursor"] = ""
|
||||
if service == "mail":
|
||||
replacements["email"] = "xinyang.dxy@alibaba-inc.com"
|
||||
replacements["folder"] = "2"
|
||||
replacements["query"] = "subject:测试"
|
||||
replacements["keyword"] = "董鑫阳"
|
||||
replacements["employee-no"] = "202397"
|
||||
replacements["size"] = "10"
|
||||
replacements["cursor"] = ""
|
||||
if command == "+find-mail-user":
|
||||
replacements["query"] = "董鑫阳"
|
||||
if service == "chat":
|
||||
replacements["group"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
|
||||
replacements["conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
|
||||
replacements["open-conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
|
||||
replacements["msg-ids"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
|
||||
replacements["message-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
|
||||
replacements["msg-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
|
||||
replacements["open-task-id"] = CHAT_FIXTURE_OPEN_TASK_ID
|
||||
if command == "+group-members":
|
||||
replacements["group"] = CHAT_FIXTURE_GROUP_NAME
|
||||
if command == "+messages-read-status":
|
||||
replacements["conversation-id"] = CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID
|
||||
replacements["users"] = "冬翔"
|
||||
if command == "+messages-resource-url":
|
||||
replacements["type"] = "mediaId"
|
||||
if command == "+bot-find":
|
||||
replacements["cursor"] = ""
|
||||
if service == "sheet" and command == "+list-sheets":
|
||||
replacements["node"] = SHEET_FIXTURE_NODE_ID
|
||||
out = list(args)
|
||||
if "--format" not in out:
|
||||
out.extend(["--format", "json"])
|
||||
i = 0
|
||||
while i < len(out) - 1:
|
||||
if out[i].startswith("--"):
|
||||
key = out[i][2:]
|
||||
if key in replacements and not out[i + 1].startswith("--"):
|
||||
out[i + 1] = replacements[key]
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def shell_join(cmd: list[str]) -> str:
|
||||
return " ".join(shlex.quote(x) for x in cmd)
|
||||
|
||||
|
||||
def drop_flag(args: list[str], flag: str) -> list[str]:
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == flag:
|
||||
i += 2 if i + 1 < len(args) and not args[i + 1].startswith("--") else 1
|
||||
continue
|
||||
out.append(args[i])
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def adjust_command_args(args: list[str], service: str, command: str) -> list[str]:
|
||||
if service == "chat" and command == "+chat-messages":
|
||||
return drop_flag(args, "--user")
|
||||
if service == "chat" and command == "+messages-list-direct":
|
||||
out = drop_flag(args, "--open-dingtalk-id")
|
||||
for i in range(len(out) - 1):
|
||||
if out[i] == "--user":
|
||||
out[i + 1] = "103262"
|
||||
return out
|
||||
if service == "calendar" and command == "+freebusy":
|
||||
return drop_flag(args, "--rooms")
|
||||
if service == "devapp" and command == "+permission-list":
|
||||
out = drop_flag(args, "--scope-value")
|
||||
out = drop_flag(out, "--scope-type")
|
||||
out = drop_flag(out, "--api-status")
|
||||
for i in range(len(out) - 1):
|
||||
if out[i] == "--auth-status":
|
||||
out[i + 1] = "ALL"
|
||||
return out
|
||||
if service == "doc" and command == "+search":
|
||||
out = drop_flag(args, "--extensions")
|
||||
out = drop_flag(out, "--created-from")
|
||||
out = drop_flag(out, "--created-to")
|
||||
out = drop_flag(out, "--visited-from")
|
||||
out = drop_flag(out, "--visited-to")
|
||||
out = drop_flag(out, "--creator-uids")
|
||||
out = drop_flag(out, "--editor-uids")
|
||||
out = drop_flag(out, "--mentioned-uids")
|
||||
out = drop_flag(out, "--workspace-ids")
|
||||
return out
|
||||
if service == "doc" and command == "+comment-list":
|
||||
out = drop_flag(args, "--cursor")
|
||||
out = drop_flag(out, "--resolve-status")
|
||||
return out
|
||||
if service == "doc" and command == "+list":
|
||||
out = drop_flag(args, "--workspace")
|
||||
out = drop_flag(out, "--cursor")
|
||||
return out
|
||||
if service == "report" and command == "+outbox-list":
|
||||
return drop_flag(args, "--template-name")
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--service", action="append", help="Only run shortcuts from this service; may repeat")
|
||||
parser.add_argument("--command", action="append", help="Only run this shortcut command; may repeat")
|
||||
parser.add_argument("--failed-only", action="store_true", help="Only rerun shortcuts currently marked non-success in the output report")
|
||||
ns = parser.parse_args()
|
||||
services = set(ns.service or [])
|
||||
commands = set(ns.command or [])
|
||||
|
||||
matrix = ensure_matrix()
|
||||
rows = [r for r in matrix["results"] if r.get("risk") == "read"]
|
||||
if services:
|
||||
rows = [r for r in rows if r["service"] in services]
|
||||
if commands:
|
||||
rows = [r for r in rows if r["command"] in commands]
|
||||
if ns.failed_only and OUT_PATH.exists():
|
||||
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
|
||||
failed = {
|
||||
(r.get("service"), r.get("command"))
|
||||
for r in existing.get("results", [])
|
||||
if r.get("status") != "real-ok"
|
||||
}
|
||||
rows = [r for r in rows if (r["service"], r["command"]) in failed]
|
||||
results = []
|
||||
summary = {"total": len(rows), "ok": 0, "error": 0, "timeout": 0, "held": 0}
|
||||
for idx, r in enumerate(rows, 1):
|
||||
key = (r["service"], r["command"])
|
||||
if key in HELD_CASES:
|
||||
summary["held"] += 1
|
||||
item = {
|
||||
"service": r["service"],
|
||||
"command": r["command"],
|
||||
"risk": r["risk"],
|
||||
"method": "held; sensitive credential read",
|
||||
"status": "held",
|
||||
"input": "",
|
||||
"args": [],
|
||||
"stdout": "",
|
||||
"stderr": HELD_CASES[key],
|
||||
"exit_code": None,
|
||||
"duration_ms": 0,
|
||||
}
|
||||
item = sanitize_result(item)
|
||||
category, fixability, note = classify_failure(item)
|
||||
item["failure_category"] = category
|
||||
item["fixability"] = fixability
|
||||
item["diagnosis"] = note
|
||||
results.append(item)
|
||||
continue
|
||||
args = adjust_command_args(replace_flag_values(r["args"], r["service"], r["command"]), r["service"], r["command"])
|
||||
cmd = [BIN] + args
|
||||
started = time.time()
|
||||
status = "real-error"
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
exit_code = None
|
||||
try:
|
||||
p = subprocess.run(cmd, text=True, capture_output=True, timeout=30)
|
||||
stdout = p.stdout.strip()
|
||||
stderr = p.stderr.strip()
|
||||
exit_code = p.returncode
|
||||
status = classify_real_status(exit_code, stdout)
|
||||
if status == "real-ok":
|
||||
status = "real-ok"
|
||||
summary["ok"] += 1
|
||||
else:
|
||||
summary["error"] += 1
|
||||
except subprocess.TimeoutExpired as e:
|
||||
status = "timeout"
|
||||
summary["timeout"] += 1
|
||||
if isinstance(e.stdout, str):
|
||||
stdout = e.stdout.strip()
|
||||
if isinstance(e.stderr, str):
|
||||
stderr = e.stderr.strip()
|
||||
duration_ms = int((time.time() - started) * 1000)
|
||||
item = {
|
||||
"service": r["service"],
|
||||
"command": r["command"],
|
||||
"risk": r["risk"],
|
||||
"method": "real-backend-read; no --mock; no --dry-run",
|
||||
"status": status,
|
||||
"input": shell_join(cmd),
|
||||
"args": args,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"exit_code": exit_code,
|
||||
"duration_ms": duration_ms,
|
||||
}
|
||||
item = sanitize_result(item)
|
||||
category, fixability, note = classify_failure(item)
|
||||
if category != "passed":
|
||||
item["failure_category"] = category
|
||||
item["fixability"] = fixability
|
||||
item["diagnosis"] = note
|
||||
results.append(item)
|
||||
if idx % 25 == 0 or idx == len(rows):
|
||||
print(
|
||||
f"progress {idx}/{len(rows)} ok={summary['ok']} "
|
||||
f"error={summary['error']} timeout={summary['timeout']} held={summary['held']}",
|
||||
flush=True,
|
||||
)
|
||||
if not services and not commands and not ns.failed_only:
|
||||
OUT_PATH.write_text(json.dumps({
|
||||
"generated_at": dt.datetime.now().isoformat(),
|
||||
"summary": summary,
|
||||
"failure_categories": summarize_failure_categories(results),
|
||||
"results": results,
|
||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
if OUT_PATH.exists() and (services or commands or ns.failed_only):
|
||||
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
|
||||
replace_keys = {(r["service"], r["command"]) for r in results}
|
||||
merged = [
|
||||
r for r in existing.get("results", [])
|
||||
if (r.get("service"), r.get("command")) not in replace_keys
|
||||
]
|
||||
merged.extend(results)
|
||||
else:
|
||||
merged = results
|
||||
summary = summarize_results(merged, include_held=True)
|
||||
OUT_PATH.write_text(json.dumps({
|
||||
"generated_at": dt.datetime.now().isoformat(),
|
||||
"summary": summary,
|
||||
"failure_categories": summarize_failure_categories(merged),
|
||||
"results": merged,
|
||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"saved {OUT_PATH} batch={summarize_results(results, include_held=True)} merged={summary}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -46,6 +46,29 @@ var expectedReleaseAdmissionContexts = []string{
|
||||
"Mock MCP",
|
||||
}
|
||||
|
||||
func TestPackageManagerVersionVerificationReadsRawBinary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "verify-package-managers.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(verify-package-managers.sh) error = %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(scriptPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
|
||||
}
|
||||
script := string(data)
|
||||
for _, binary := range []string{`"$vendor_bin"`, `"$prefix/bin/dws"`} {
|
||||
want := `LC_ALL=C grep -aFq "v$EXPECTED_VERSION" ` + binary
|
||||
if !strings.Contains(script, want) {
|
||||
t.Errorf("package-manager verifier is missing raw binary marker check %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(script, `strings "$vendor_bin"`) || strings.Contains(script, `strings "$prefix/bin/dws"`) {
|
||||
t.Fatal("package-manager verifier still requires the version marker to occupy a strings(1) line")
|
||||
}
|
||||
}
|
||||
|
||||
func seedDistArchive(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
file, err := os.Create(path)
|
||||
|
||||
Reference in New Issue
Block a user