Merge remote-tracking branch 'origin/main' into fix/param-hallucination

# Conflicts:
#	internal/cli/param_concepts.json
This commit is contained in:
克谨 committed 2026-08-05 16:23:01 +08:00
commit 1231e500a3
130 files changed
+11058 -3804

No files matched your search

+51 -2
View File
@@ -24,7 +24,10 @@ import gen_shortcut_comparison as shortcut_source # noqa: E402
CATALOG_PATH = ROOT / "docs" / "shortcut-public-catalog.json"
MONO_SKILL = ROOT / "skills" / "mono" / "SKILL.md"
SHARED_SKILL = ROOT / "skills" / "multi" / "dws-shared" / "SKILL.md"
RUNTIME_CONTRACT_SOURCE = (
ROOT / "skills" / "multi" / "dws-shared" / "references" / "runtime-contract.md"
)
SERVICE_TO_SKILL = {
"aitable": ROOT / "skills" / "multi" / "dingtalk-aitable" / "SKILL.md",
"attendance": ROOT / "skills" / "multi" / "dingtalk-misc" / "references" / "attendance.md",
@@ -48,6 +51,8 @@ MONO_START = "<!-- VISIBLE_SHORTCUTS_OVERVIEW_START -->"
MONO_END = "<!-- VISIBLE_SHORTCUTS_OVERVIEW_END -->"
PRODUCT_START = "<!-- VISIBLE_SHORTCUTS_START -->"
PRODUCT_END = "<!-- VISIBLE_SHORTCUTS_END -->"
RUNTIME_CONTRACT_START = "<!-- DWS_RUNTIME_CONTRACT_START -->"
RUNTIME_CONTRACT_END = "<!-- DWS_RUNTIME_CONTRACT_END -->"
# Large, high-frequency product skills should route known intents directly and
# keep their full shortcut inventory in Runtime Catalog/Schema. Add services
@@ -92,6 +97,29 @@ def replace_block(text: str, start: str, end: str, block: str, fallback_anchor:
return text.replace(fallback_anchor, block + "\n\n" + fallback_anchor, 1)
def replace_required_block(text: str, start: str, end: str, block: str) -> str:
if text.count(start) != 1 or text.count(end) != 1:
raise RuntimeError(
f"expected exactly one generated block {start!r} ... {end!r}"
)
before = text.split(start, 1)[0]
after = text.split(end, 1)[1]
return before + block + after
def runtime_contract_block() -> str:
contract = RUNTIME_CONTRACT_SOURCE.read_text(encoding="utf-8").strip()
if not contract.startswith("## 最小 DWS 执行契约"):
raise RuntimeError(
f"runtime contract must start with its canonical heading: {RUNTIME_CONTRACT_SOURCE}"
)
return (
f"{RUNTIME_CONTRACT_START}\n"
f"{contract}\n"
f"{RUNTIME_CONTRACT_END}"
)
def mono_overview(items: list[dict[str, Any]]) -> str:
counts = Counter(item["service"] for item in items)
rows = []
@@ -161,6 +189,26 @@ def update_mono(items: list[dict[str, Any]], check: bool) -> list[Path]:
return [MONO_SKILL] if apply_update(MONO_SKILL, text, updated, check) else []
def update_runtime_contract(check: bool) -> list[Path]:
block = runtime_contract_block()
changed = []
targets = [
ROOT / "skills" / "multi" / "dingtalk-chat" / "SKILL.md",
SHARED_SKILL,
]
for path in targets:
text = path.read_text(encoding="utf-8")
updated = replace_required_block(
text,
RUNTIME_CONTRACT_START,
RUNTIME_CONTRACT_END,
block,
)
if apply_update(path, text, updated, check):
changed.append(path)
return changed
def update_product_skills(items: list[dict[str, Any]], check: bool) -> list[Path]:
by_service: dict[str, list[dict[str, Any]]] = defaultdict(list)
for item in items:
@@ -190,7 +238,8 @@ def main() -> int:
args = parser.parse_args()
items = collect_visible()
changed = update_mono(items, args.check)
changed = update_runtime_contract(args.check)
changed.extend(update_mono(items, args.check))
changed.extend(update_product_skills(items, args.check))
if args.check and changed:
print("run: python3 scripts/gen_skill_shortcut_sections.py", file=sys.stderr)
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
cd "$ROOT"
. "$ROOT/scripts/policy/policy-runtime.sh"
policy_prepare_runtime "$ROOT"
TMP_ROOT="$(policy_runtime_mktemp_dir dws-multi-im-skill-chain)"
CHECKER="$TMP_ROOT/multi-im-skill-chain"
CHECK_HOME="$TMP_ROOT/home"
mkdir -p "$CHECK_HOME"
trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
go build -o "$CHECKER" ./scripts/policy/multi-im-skill-chain
HOME="$CHECK_HOME" DWS_LANG=zh "$CHECKER"
+70 -2
View File
@@ -7,8 +7,12 @@ cd "$ROOT"
python3 scripts/gen_skill_shortcut_sections.py --check
chat_skill="skills/multi/dingtalk-chat/SKILL.md"
event_skill="skills/multi/dingtalk-event/SKILL.md"
mono_skill="skills/mono/SKILL.md"
chat_max_bytes=14000
runtime_contract="skills/multi/dws-shared/references/runtime-contract.md"
chat_max_bytes=10000
event_max_bytes=10000
runtime_contract_max_bytes=3000
chat_bytes="$(wc -c < "$chat_skill" | tr -d ' ')"
if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
@@ -17,6 +21,20 @@ if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
exit 1
fi
event_bytes="$(wc -c < "$event_skill" | tr -d ' ')"
if [ "$event_bytes" -gt "$event_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $event_skill is ${event_bytes} bytes (max ${event_max_bytes})" >&2
exit 1
fi
runtime_contract_bytes="$(wc -c < "$runtime_contract" | tr -d ' ')"
if [ "$runtime_contract_bytes" -gt "$runtime_contract_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $runtime_contract is ${runtime_contract_bytes} bytes (max ${runtime_contract_max_bytes})" >&2
exit 1
fi
shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
@@ -31,6 +49,56 @@ if [ "$shortcut_rows" -ne 0 ]; then
exit 1
fi
for required_heading in \
"## 最小 DWS 执行契约" \
"## Golden Route" \
"## 关键结果语义" \
"## 按需加载" \
"## 错误最短路径"
do
if ! grep -Fq "$required_heading" "$chat_skill"; then
printf '%s\n' \
"skill value regression: $chat_skill is missing $required_heading" >&2
exit 1
fi
done
for required_route in \
"+dm" \
"+send-to-group" \
"+messages-send" \
"+chat-messages" \
"+search-msg" \
"--download-resources" \
"+conversation-list-top"
do
if ! grep -Fq -- "$required_route" "$chat_skill"; then
printf '%s\n' \
"skill route regression: $chat_skill is missing $required_route" >&2
exit 1
fi
done
for forbidden_route in \
"## 标准 SOP" \
"dws aisearch person --keyword" \
"dws chat message send-by-webhook" \
"dt_media_upload" \
"MUST 先用 Read"
do
if grep -Fq "$forbidden_route" "$chat_skill"; then
printf '%s\n' \
"skill route regression: $chat_skill restored legacy route: $forbidden_route" >&2
exit 1
fi
done
if grep -Fq "../dws-shared/SKILL.md" "$chat_skill"; then
printf '%s\n' \
"skill context regression: $chat_skill requires full dws-shared cold-start loading" >&2
exit 1
fi
if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
printf '%s\n' \
"skill context budget regression: $mono_skill requires full product-reference loading" >&2
@@ -38,4 +106,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
fi
printf '%s\n' \
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows)"
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows)"
+495
View File
@@ -0,0 +1,495 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// Command multi-im-skill-chain verifies that reviewed high-frequency IM
// intents keep one default route across Skill references and Agent selection.
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
"github.com/spf13/cobra"
)
const manifestRelativePath = "scripts/policy/multi-im-skill-chain/testdata/intent_routes.json"
var (
intentIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*$`)
reasonCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
intentMarker = regexp.MustCompile(`<!--\s*dws-intent:\s*([a-z0-9][a-z0-9._-]*)\s*-->`)
mainGetwd = os.Getwd
mainExit = os.Exit
markerOpen = os.Open
buildEffective = cli.BuildEffectiveCommandRegistry
bindEffective = cli.BindEffectiveCommandRegistry
)
type routeManifest struct {
Version int `json:"version"`
MarkerRoots []string `json:"marker_roots"`
Intents []intentRoute `json:"intents"`
RetiredScripts []string `json:"retired_scripts"`
ContractReference string `json:"contract_reference"`
HandoffReference string `json:"handoff_reference"`
}
type intentRoute struct {
ID string `json:"id"`
PreferredTool string `json:"preferred_tool"`
AllowedFallbacks []routeFallback `json:"allowed_fallbacks,omitempty"`
ForbiddenDefaultTools []string `json:"forbidden_default_tools,omitempty"`
References []string `json:"references"`
}
type routeFallback struct {
Tool string `json:"tool"`
ReasonCode string `json:"reason_code"`
}
type toolFact struct {
Canonical string
PrimaryPath string
Confirmation string
UseWhen []string
AvoidWhen []string
HasMeta bool
}
func main() {
rootPath, err := mainGetwd()
if err != nil {
fmt.Fprintln(os.Stderr, err)
mainExit(2)
return
}
mainExit(run(rootPath, app.NewRootCommand(), os.Stdout, os.Stderr))
}
func run(rootPath string, root *cobra.Command, stdout, stderr io.Writer) int {
manifest, err := loadManifest(filepath.Join(rootPath, manifestRelativePath))
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
effective, err := buildEffective(root)
if err != nil {
fmt.Fprintf(stderr, "build effective CommandRegistry: %v\n", err)
return 2
}
bound, err := bindEffective(root, effective)
if err != nil {
fmt.Fprintf(stderr, "bind effective CommandRegistry: %v\n", err)
return 2
}
tools := make(map[string]toolFact, len(bound.ByCanonical))
for canonical, item := range bound.ByCanonical {
meta, ok := cli.ResolveMeta(item.PrimaryCLIPath)
tools[canonical] = toolFact{
Canonical: canonical,
PrimaryPath: item.PrimaryCLIPath,
Confirmation: meta.Safety.Confirmation,
UseWhen: append([]string(nil), meta.Selection.UseWhen...),
AvoidWhen: append([]string(nil), meta.Selection.AvoidWhen...),
HasMeta: ok,
}
}
failures := validateManifest(rootPath, manifest, tools)
sort.Strings(failures)
if len(failures) > 0 {
fmt.Fprintf(stderr, "multi IM Skill chain check failed (%d problems):\n", len(failures))
for _, failure := range failures {
fmt.Fprintf(stderr, " - %s\n", failure)
}
return 1
}
fmt.Fprintf(stdout, "multi IM Skill chain check: ok (%d intents)\n", len(manifest.Intents))
return 0
}
func loadManifest(path string) (routeManifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return routeManifest{}, fmt.Errorf("read IM intent route manifest: %w", err)
}
decoder := json.NewDecoder(strings.NewReader(string(data)))
decoder.DisallowUnknownFields()
var manifest routeManifest
if err := decoder.Decode(&manifest); err != nil {
return routeManifest{}, fmt.Errorf("decode IM intent route manifest: %w", err)
}
return manifest, nil
}
func validateManifest(rootPath string, manifest routeManifest, tools map[string]toolFact) []string {
var failures []string
if manifest.Version != 3 {
failures = append(failures, fmt.Sprintf("manifest version = %d, want 3", manifest.Version))
}
if len(manifest.MarkerRoots) == 0 {
failures = append(failures, "manifest marker_roots must not be empty")
}
if len(manifest.Intents) == 0 {
failures = append(failures, "manifest intents must not be empty")
}
failures = append(failures, validateRetiredScripts(rootPath, manifest.RetiredScripts)...)
failures = append(failures, validateTypedContractReference(rootPath, manifest.ContractReference)...)
failures = append(failures, validateEventHandoffReference(rootPath, manifest.HandoffReference)...)
intentByID := make(map[string]intentRoute, len(manifest.Intents))
for _, route := range manifest.Intents {
if !intentIDPattern.MatchString(route.ID) {
failures = append(failures, fmt.Sprintf("intent id %q is invalid", route.ID))
continue
}
if _, exists := intentByID[route.ID]; exists {
failures = append(failures, fmt.Sprintf("duplicate intent id %q", route.ID))
continue
}
intentByID[route.ID] = route
preferred, ok := tools[route.PreferredTool]
if !ok {
failures = append(failures, fmt.Sprintf("intent %s preferred tool %q is absent from BoundCommandRegistry", route.ID, route.PreferredTool))
continue
}
if !preferred.HasMeta || preferred.Confirmation == "" {
failures = append(failures, fmt.Sprintf("intent %s preferred tool %q is absent from ResolveMeta delivery", route.ID, route.PreferredTool))
}
if len(preferred.UseWhen) == 0 || len(preferred.AvoidWhen) == 0 {
failures = append(failures, fmt.Sprintf("intent %s preferred tool %q needs non-empty use_when and avoid_when", route.ID, route.PreferredTool))
}
seenFallbacks := map[string]bool{}
for _, fallback := range route.AllowedFallbacks {
if fallback.Tool == "" || seenFallbacks[fallback.Tool] {
failures = append(failures, fmt.Sprintf("intent %s has empty or duplicate allowed fallback %q", route.ID, fallback.Tool))
continue
}
seenFallbacks[fallback.Tool] = true
if !reasonCodePattern.MatchString(fallback.ReasonCode) {
failures = append(failures, fmt.Sprintf("intent %s fallback %q has invalid reason_code %q", route.ID, fallback.Tool, fallback.ReasonCode))
}
fact, exists := tools[fallback.Tool]
if !exists {
failures = append(failures, fmt.Sprintf("intent %s fallback tool %q is absent from BoundCommandRegistry", route.ID, fallback.Tool))
continue
}
if !fact.HasMeta || fact.Confirmation == "" {
failures = append(failures, fmt.Sprintf("intent %s fallback tool %q is absent from ResolveMeta delivery", route.ID, fallback.Tool))
} else if fact.Confirmation != preferred.Confirmation {
failures = append(failures, fmt.Sprintf("intent %s fallback %q confirmation %q differs from preferred %q confirmation %q", route.ID, fallback.Tool, fact.Confirmation, route.PreferredTool, preferred.Confirmation))
}
}
seenForbidden := map[string]bool{}
for _, canonical := range route.ForbiddenDefaultTools {
if canonical == "" || seenForbidden[canonical] {
failures = append(failures, fmt.Sprintf("intent %s has empty or duplicate forbidden default %q", route.ID, canonical))
continue
}
seenForbidden[canonical] = true
entry, exists := tools[canonical]
if !exists {
failures = append(failures, fmt.Sprintf("intent %s forbidden default tool %q is absent from BoundCommandRegistry", route.ID, canonical))
continue
}
if !entry.HasMeta || entry.Confirmation == "" {
failures = append(failures, fmt.Sprintf("intent %s forbidden default tool %q is absent from ResolveMeta delivery", route.ID, canonical))
}
if len(entry.UseWhen) == 0 || len(entry.AvoidWhen) == 0 {
failures = append(failures, fmt.Sprintf("intent %s forbidden default tool %q needs non-empty use_when and avoid_when", route.ID, canonical))
}
}
seenReferences := map[string]bool{}
for _, reference := range route.References {
if !safeRepositoryPath(reference) || filepath.Ext(reference) != ".md" {
failures = append(failures, fmt.Sprintf("intent %s has invalid reference %q", route.ID, reference))
continue
}
if seenReferences[reference] {
failures = append(failures, fmt.Sprintf("intent %s repeats reference %q", route.ID, reference))
continue
}
seenReferences[reference] = true
if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(reference))); err != nil {
failures = append(failures, fmt.Sprintf("intent %s reference %q does not exist", route.ID, reference))
}
}
}
markerFailures, markers := scanMarkers(rootPath, manifest.MarkerRoots, intentByID, tools)
failures = append(failures, markerFailures...)
for _, route := range manifest.Intents {
for _, reference := range route.References {
key := route.ID + "\x00" + filepath.ToSlash(reference)
switch markers[key] {
case 0:
failures = append(failures, fmt.Sprintf("intent %s reference %s is missing its dws-intent marker", route.ID, reference))
case 1:
default:
failures = append(failures, fmt.Sprintf("intent %s reference %s has %d dws-intent markers, want 1", route.ID, reference, markers[key]))
}
}
}
return failures
}
func validateRetiredScripts(rootPath string, paths []string) []string {
var failures []string
if len(paths) == 0 {
return []string{"manifest retired_scripts must not be empty"}
}
seen := map[string]bool{}
for _, path := range paths {
if !safeRepositoryPath(path) || filepath.Ext(path) != ".py" {
failures = append(failures, fmt.Sprintf("invalid retired script path %q", path))
continue
}
if seen[path] {
failures = append(failures, fmt.Sprintf("duplicate retired script path %q", path))
continue
}
seen[path] = true
if _, err := os.Stat(filepath.Join(rootPath, filepath.FromSlash(path))); err == nil {
failures = append(failures, fmt.Sprintf("retired script %s was republished", path))
} else if !os.IsNotExist(err) {
failures = append(failures, fmt.Sprintf("inspect retired script %s: %v", path, err))
}
}
return failures
}
func validateTypedContractReference(rootPath, relative string) []string {
if !safeRepositoryPath(relative) || filepath.Ext(relative) != ".md" {
return []string{fmt.Sprintf("invalid contract_reference %q", relative)}
}
data, err := os.ReadFile(filepath.Join(rootPath, filepath.FromSlash(relative)))
if err != nil {
return []string{fmt.Sprintf("read typed contract reference %s: %v", relative, err)}
}
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
blocks := []struct {
name string
expected string
}{
{name: "MESSAGE_RESULT", expected: renderMessageResultContract()},
{name: "IDENTITY_CAPABILITY", expected: renderIdentityCapabilityContract()},
{name: "CARD_WORKFLOW", expected: renderCardWorkflowContract()},
{name: "CAPABILITY_BOUNDARY", expected: renderCapabilityBoundaryContract()},
}
var failures []string
for _, block := range blocks {
start := "<!-- DWS_" + block.name + "_CONTRACT_START -->"
end := "<!-- DWS_" + block.name + "_CONTRACT_END -->"
expected := start + "\n" + block.expected + "\n" + end
if bytes.Count(data, []byte(start)) != 1 || bytes.Count(data, []byte(end)) != 1 {
failures = append(failures, fmt.Sprintf("%s must contain exactly one %s contract marker pair", relative, block.name))
continue
}
startAt := bytes.Index(data, []byte(start))
endAt := bytes.Index(data[startAt:], []byte(end))
if endAt < 0 {
failures = append(failures, fmt.Sprintf("%s has malformed %s contract markers", relative, block.name))
continue
}
actual := string(data[startAt : startAt+endAt+len(end)])
if actual != expected {
failures = append(failures, fmt.Sprintf("%s %s contract differs from Runtime typed descriptor", relative, block.name))
}
}
return failures
}
func validateEventHandoffReference(rootPath, relative string) []string {
if !safeRepositoryPath(relative) || filepath.Ext(relative) != ".md" {
return []string{fmt.Sprintf("invalid handoff_reference %q", relative)}
}
data, err := os.ReadFile(filepath.Join(rootPath, filepath.FromSlash(relative)))
if err != nil {
return []string{fmt.Sprintf("read event handoff reference %s: %v", relative, err)}
}
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
const expected = `<!-- DWS_EVENT_CHAT_HANDOFF_START -->
| event field | exact chat target |
|---|---|
| ` + "`conversation_id`" + ` | ` + "`dws chat +messages-send --as user --group <conversation_id>`" + ` |
| ` + "`sender_open_dingtalk_id`" + ` | ` + "`dws chat +messages-send --as user --open-dingtalk-id <sender_open_dingtalk_id>`" + ` |
<!-- DWS_EVENT_CHAT_HANDOFF_END -->`
start := bytes.Index(data, []byte("<!-- DWS_EVENT_CHAT_HANDOFF_START -->"))
endMarker := "<!-- DWS_EVENT_CHAT_HANDOFF_END -->"
if start < 0 || bytes.Count(data, []byte("<!-- DWS_EVENT_CHAT_HANDOFF_START -->")) != 1 ||
bytes.Count(data, []byte(endMarker)) != 1 {
return []string{fmt.Sprintf("%s must contain exactly one event-to-chat handoff marker pair", relative)}
}
end := bytes.Index(data[start:], []byte(endMarker))
if end < 0 || string(data[start:start+end+len(endMarker)]) != expected {
return []string{fmt.Sprintf("%s event-to-chat handoff differs from exact stable-ID mapping", relative)}
}
return nil
}
func renderMessageResultContract() string {
contract := chatmsg.CurrentMessageResultContract()
return fmt.Sprintf("- `version`: `%s`\n- `message_fields`: %s\n- `envelope_fields`: %s",
contract.Version, markdownCodeList(contract.MessageFields), markdownCodeList(contract.EnvelopeFields))
}
func renderIdentityCapabilityContract() string {
var rows []string
for _, capability := range chatshortcut.MessageIdentityCapabilities() {
rows = append(rows, fmt.Sprintf("| `%s` | %s | %s | %s | %s | `%t` | `%t` |",
capability.Identity, markdownBreakList(capability.Targets), markdownBreakList(capability.ContentTypes),
markdownBreakList(capability.NaturalTargets), markdownBreakList(capability.MentionTargets),
capability.IdempotencyKeys, capability.BatchLedger))
}
return "| identity | targets | content types | natural targets | mention targets | idempotency keys | batch ledger |\n" +
"|---|---|---|---|---|---:|---:|\n" + strings.Join(rows, "\n")
}
func renderCardWorkflowContract() string {
contract := chatshortcut.CurrentCardWorkflowContract()
statuses := make([]string, 0, len(contract.FlowStatuses))
for _, status := range contract.FlowStatuses {
statuses = append(statuses, fmt.Sprintf("%d=%s", status.Value, status.Name))
}
return fmt.Sprintf("- `version`: `%s`\n- `targets`: %s\n- `content_types`: %s\n- `flow_statuses`: %s\n- `callback_supported`: `%t`",
contract.Version, markdownCodeList(contract.Targets), markdownCodeList(contract.ContentTypes),
markdownCodeList(statuses), contract.CallbackSupported)
}
func renderCapabilityBoundaryContract() string {
rows := make([]string, 0)
for _, boundary := range chatshortcut.CurrentIMCapabilityBoundaries() {
rows = append(rows, fmt.Sprintf("| `%s` | `%t` | %s |", boundary.Capability, boundary.Supported, boundary.Alternative))
}
return "| capability | supported | current route / boundary |\n|---|---:|---|\n" + strings.Join(rows, "\n")
}
func markdownCodeList(values []string) string {
if len(values) == 0 {
return "—"
}
quoted := make([]string, len(values))
for index, value := range values {
quoted[index] = "`" + value + "`"
}
return strings.Join(quoted, ", ")
}
func markdownBreakList(values []string) string {
if len(values) == 0 {
return "—"
}
quoted := make([]string, len(values))
for index, value := range values {
quoted[index] = "`" + value + "`"
}
return strings.Join(quoted, "<br>")
}
func scanMarkers(rootPath string, roots []string, intents map[string]intentRoute, tools map[string]toolFact) ([]string, map[string]int) {
var failures []string
markers := map[string]int{}
for _, relativeRoot := range roots {
if !safeRepositoryPath(relativeRoot) {
failures = append(failures, fmt.Sprintf("invalid marker root %q", relativeRoot))
continue
}
absoluteRoot := filepath.Join(rootPath, filepath.FromSlash(relativeRoot))
err := filepath.WalkDir(absoluteRoot, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || filepath.Ext(path) != ".md" {
return nil
}
file, err := markerOpen(path)
if err != nil {
return err
}
defer file.Close()
relative, _ := filepath.Rel(rootPath, path)
relative = filepath.ToSlash(relative)
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := scanner.Text()
for _, match := range intentMarker.FindAllStringSubmatch(line, -1) {
id := match[1]
route, ok := intents[id]
if !ok {
failures = append(failures, fmt.Sprintf("%s:%d uses unknown dws-intent %q", relative, lineNumber, id))
continue
}
if !stringSliceContains(route.References, relative) {
failures = append(failures, fmt.Sprintf("%s:%d uses undeclared dws-intent %q", relative, lineNumber, id))
}
markers[id+"\x00"+relative]++
preferred, exists := tools[route.PreferredTool]
if !exists {
continue
}
if !containsCLIPath(line, preferred.PrimaryPath) {
failures = append(failures, fmt.Sprintf("%s:%d intent %s must contain preferred path `dws %s` on the marker line", relative, lineNumber, id, preferred.PrimaryPath))
}
for _, canonical := range route.ForbiddenDefaultTools {
if fact, ok := tools[canonical]; ok && containsCLIPath(line, fact.PrimaryPath) {
failures = append(failures, fmt.Sprintf("%s:%d intent %s uses forbidden default `dws %s`", relative, lineNumber, id, fact.PrimaryPath))
}
}
}
}
return scanner.Err()
})
if err != nil {
failures = append(failures, fmt.Sprintf("scan marker root %s: %v", relativeRoot, err))
}
}
return failures, markers
}
func safeRepositoryPath(path string) bool {
path = filepath.ToSlash(strings.TrimSpace(path))
return path != "" && path != "." && !filepath.IsAbs(path) && path != ".." && !strings.HasPrefix(path, "../") && !strings.Contains(path, "/../")
}
func containsCLIPath(line, path string) bool {
needle := "dws " + strings.TrimSpace(path)
index := strings.Index(line, needle)
if index < 0 {
return false
}
end := index + len(needle)
if end == len(line) {
return true
}
next := line[end]
return next == ' ' || next == '`' || next == '<' || next == '|' || next == ','
}
func stringSliceContains(values []string, target string) bool {
for _, value := range values {
if filepath.ToSlash(value) == target {
return true
}
}
return false
}
@@ -0,0 +1,391 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageRunRepositoryAndSetupFailures(t *testing.T) {
root, err := filepath.Abs("../../..")
if err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
if code := run(root, app.NewRootCommand(), &stdout, &stderr); code != 0 {
t.Fatalf("repository policy run = %d, stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), "multi IM Skill chain check: ok") {
t.Fatalf("success output = %q", stdout.String())
}
stdout.Reset()
stderr.Reset()
if code := run(t.TempDir(), &cobra.Command{Use: "dws"}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "read IM intent route manifest") {
t.Fatalf("missing-manifest run = %d, stderr=%s", code, stderr.String())
}
stdout.Reset()
stderr.Reset()
if code := run(root, &cobra.Command{Use: "dws"}, &stdout, &stderr); code != 1 || !strings.Contains(stderr.String(), "absent from BoundCommandRegistry") {
t.Fatalf("unbound-root run = %d, stderr=%s", code, stderr.String())
}
oldBuild, oldBind := buildEffective, bindEffective
t.Cleanup(func() { buildEffective, bindEffective = oldBuild, oldBind })
buildEffective = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) {
return cli.EffectiveCommandRegistry{}, os.ErrInvalid
}
stdout.Reset()
stderr.Reset()
if code := run(root, app.NewRootCommand(), &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "build effective CommandRegistry") {
t.Fatalf("registry-build-failure run = %d, stderr=%s", code, stderr.String())
}
buildEffective = oldBuild
bindEffective = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) {
return cli.BoundCommandRegistry{}, os.ErrInvalid
}
stdout.Reset()
stderr.Reset()
if code := run(root, app.NewRootCommand(), &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "bind effective CommandRegistry") {
t.Fatalf("binding-failure run = %d, stderr=%s", code, stderr.String())
}
bindEffective = oldBind
failureRoot := t.TempDir()
writeTestFile(t, failureRoot, manifestRelativePath, `{"version":1}`)
stdout.Reset()
stderr.Reset()
if code := run(failureRoot, app.NewRootCommand(), &stdout, &stderr); code != 1 || !strings.Contains(stderr.String(), "multi IM Skill chain check failed") {
t.Fatalf("validation-failure run = %d, stderr=%s", code, stderr.String())
}
}
func TestCrossPlatformCoverageMainUsesInjectableProcessBoundaries(t *testing.T) {
oldGetwd, oldExit := mainGetwd, mainExit
t.Cleanup(func() { mainGetwd, mainExit = oldGetwd, oldExit })
var exitCode int
mainExit = func(code int) { exitCode = code }
mainGetwd = func() (string, error) { return "", os.ErrPermission }
main()
if exitCode != 2 {
t.Fatalf("getwd failure exit = %d", exitCode)
}
root, err := filepath.Abs("../../..")
if err != nil {
t.Fatal(err)
}
mainGetwd = func() (string, error) { return root, nil }
main()
if exitCode != 0 {
t.Fatalf("successful main exit = %d", exitCode)
}
}
func TestCrossPlatformCoverageValidateManifestAcceptsExactReviewedRoute(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "skills/multi/dingtalk-chat/SKILL.md", "| 发消息 | `dws chat +send` | <!-- dws-intent: chat.send -->\n")
writeTypedContractReference(t, root, "skills/multi/dingtalk-chat/references/contracts.md")
writeEventHandoffReference(t, root, "skills/multi/dingtalk-event/references/event-im-output.md")
manifest := routeManifest{
Version: 3,
MarkerRoots: []string{"skills/multi/dingtalk-chat"},
RetiredScripts: []string{"skills/multi/dingtalk-chat/scripts/retired.py"},
ContractReference: "skills/multi/dingtalk-chat/references/contracts.md",
HandoffReference: "skills/multi/dingtalk-event/references/event-im-output.md",
Intents: []intentRoute{{
ID: "chat.send",
PreferredTool: "chat.shortcut_send",
ForbiddenDefaultTools: []string{"chat.atomic_send"},
References: []string{"skills/multi/dingtalk-chat/SKILL.md"},
}},
}
tools := map[string]toolFact{
"chat.shortcut_send": {Canonical: "chat.shortcut_send", PrimaryPath: "chat +send", Confirmation: "user_required", UseWhen: []string{"普通发送"}, AvoidWhen: []string{"底层字段另选"}, HasMeta: true},
"chat.atomic_send": {Canonical: "chat.atomic_send", PrimaryPath: "chat message send", Confirmation: "not_required", UseWhen: []string{"底层字段"}, AvoidWhen: []string{"普通发送使用 +send"}, HasMeta: true},
}
if failures := validateManifest(root, manifest, tools); len(failures) != 0 {
t.Fatalf("failures = %v", failures)
}
}
func TestCrossPlatformCoverageValidateManifestRejectsWrongMarkerAndSafetyDowngrade(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "skills/multi/dingtalk-chat/SKILL.md", "| 发消息 | `dws chat message send` | <!-- dws-intent: chat.send -->\n")
writeTypedContractReference(t, root, "skills/multi/dingtalk-chat/references/contracts.md")
writeEventHandoffReference(t, root, "skills/multi/dingtalk-event/references/event-im-output.md")
manifest := routeManifest{
Version: 3,
MarkerRoots: []string{"skills/multi/dingtalk-chat"},
RetiredScripts: []string{"skills/multi/dingtalk-chat/scripts/retired.py"},
ContractReference: "skills/multi/dingtalk-chat/references/contracts.md",
HandoffReference: "skills/multi/dingtalk-event/references/event-im-output.md",
Intents: []intentRoute{{
ID: "chat.send",
PreferredTool: "chat.shortcut_send",
AllowedFallbacks: []routeFallback{{Tool: "chat.atomic_send", ReasonCode: "raw_field"}},
ForbiddenDefaultTools: []string{"chat.atomic_send"},
References: []string{"skills/multi/dingtalk-chat/SKILL.md"},
}},
}
tools := map[string]toolFact{
"chat.shortcut_send": {Canonical: "chat.shortcut_send", PrimaryPath: "chat +send", Confirmation: "user_required", UseWhen: []string{"普通发送"}, AvoidWhen: []string{"底层字段另选"}, HasMeta: true},
"chat.atomic_send": {Canonical: "chat.atomic_send", PrimaryPath: "chat message send", Confirmation: "not_required", UseWhen: []string{"普通发送"}, HasMeta: true},
}
failures := strings.Join(validateManifest(root, manifest, tools), "\n")
for _, want := range []string{"confirmation", "needs non-empty use_when and avoid_when", "must contain preferred path", "uses forbidden default"} {
if !strings.Contains(failures, want) {
t.Fatalf("failures = %s, want %q", failures, want)
}
}
}
func TestCrossPlatformCoverageLoadManifestRejectsUnknownFields(t *testing.T) {
path := filepath.Join(t.TempDir(), "manifest.json")
if err := os.WriteFile(path, []byte(`{"version":1,"unknown":true}`), 0o600); err != nil {
t.Fatal(err)
}
if _, err := loadManifest(path); err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("error = %v", err)
}
}
func TestCrossPlatformCoverageValidateRetiredScriptsRejectsRepublishedAndDuplicatePaths(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "skills/multi/dingtalk-chat/scripts/export.py", "republished\n")
failures := strings.Join(validateRetiredScripts(root, []string{
"skills/multi/dingtalk-chat/scripts/export.py",
"skills/multi/dingtalk-chat/scripts/export.py",
"../unsafe.py",
}), "\n")
for _, want := range []string{"was republished", "duplicate retired", "invalid retired"} {
if !strings.Contains(failures, want) {
t.Fatalf("failures = %s, want %q", failures, want)
}
}
}
func TestCrossPlatformCoverageValidateTypedContractReferenceRejectsDrift(t *testing.T) {
root := t.TempDir()
writeTypedContractReference(t, root, "contracts.md")
path := filepath.Join(root, "contracts.md")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
data = []byte(strings.Replace(string(data), "`im.message-list.v1`", "`drifted`", 1))
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
failures := strings.Join(validateTypedContractReference(root, "contracts.md"), "\n")
if !strings.Contains(failures, "MESSAGE_RESULT contract differs") {
t.Fatalf("failures = %s", failures)
}
}
func TestCrossPlatformCoverageValidateEventHandoffReferenceRejectsNaturalTargetDrift(t *testing.T) {
root := t.TempDir()
writeEventHandoffReference(t, root, "handoff.md")
path := filepath.Join(root, "handoff.md")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
data = []byte(strings.Replace(string(data), "--group <conversation_id>", "--chat-query <display_name>", 1))
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
failures := strings.Join(validateEventHandoffReference(root, "handoff.md"), "\n")
if !strings.Contains(failures, "exact stable-ID mapping") {
t.Fatalf("failures = %s", failures)
}
}
func TestCrossPlatformCoverageValidateReferencesAcceptCRLF(t *testing.T) {
root := t.TempDir()
for _, tc := range []struct {
path string
write func(*testing.T, string, string)
validate func(string, string) []string
}{
{path: "contracts.md", write: writeTypedContractReference, validate: validateTypedContractReference},
{path: "handoff.md", write: writeEventHandoffReference, validate: validateEventHandoffReference},
} {
tc.write(t, root, tc.path)
path := filepath.Join(root, tc.path)
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
data = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n"))
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
if failures := tc.validate(root, tc.path); len(failures) != 0 {
t.Errorf("%s CRLF failures = %v", tc.path, failures)
}
}
}
func TestCrossPlatformCoverageManifestFailureMatrix(t *testing.T) {
root := t.TempDir()
writeTypedContractReference(t, root, "contracts.md")
writeEventHandoffReference(t, root, "handoff.md")
writeTestFile(t, root, "refs/declared.md", "<!-- dws-intent: declared --> missing preferred; dws chat forbidden\n<!-- dws-intent: declared --> duplicate\n")
writeTestFile(t, root, "refs/markers.md", "<!-- dws-intent: unknown -->\n<!-- dws-intent: declared -->\n<!-- dws-intent: preferred-absent -->\n")
tools := map[string]toolFact{
"preferred.empty": {PrimaryPath: "chat preferred", HasMeta: false},
"fallback.empty": {PrimaryPath: "chat fallback", HasMeta: false},
"fallback.different": {PrimaryPath: "chat fallback-different", HasMeta: true, Confirmation: "not_required"},
"forbidden.no-route": {PrimaryPath: "chat forbidden", HasMeta: true, Confirmation: "not_required", AvoidWhen: []string{"keep using lower tool"}},
"forbidden.no-entry": {PrimaryPath: "chat forbidden-missing"},
}
manifest := routeManifest{
Version: 1,
MarkerRoots: []string{"refs", "../unsafe", "missing-root"},
RetiredScripts: nil,
ContractReference: "contracts.md",
HandoffReference: "handoff.md",
Intents: []intentRoute{
{ID: "INVALID ID"},
{ID: "preferred-absent", PreferredTool: "absent"},
{ID: "preferred-no-selection", PreferredTool: "forbidden.no-entry"},
{
ID: "declared", PreferredTool: "preferred.empty",
AllowedFallbacks: []routeFallback{
{},
{Tool: "fallback.empty", ReasonCode: "BAD-CODE"},
{Tool: "fallback.empty", ReasonCode: "duplicate"},
{Tool: "fallback.absent", ReasonCode: "missing"},
{Tool: "fallback.different", ReasonCode: "raw_field"},
},
ForbiddenDefaultTools: []string{"", "forbidden.absent", "forbidden.no-entry", "forbidden.no-route", "forbidden.no-route"},
References: []string{"../unsafe.md", "refs/wrong.txt", "refs/missing.md", "refs/declared.md", "refs/declared.md"},
},
{ID: "declared"},
},
}
failures := strings.Join(validateManifest(root, manifest, tools), "\n")
for _, want := range []string{
"manifest version", "retired_scripts", "intent id",
"preferred tool", "ResolveMeta", "needs non-empty", "allowed fallback", "reason_code", "fallback tool",
"confirmation", "forbidden default", "invalid reference", "does not exist", "repeats reference",
"unknown dws-intent", "undeclared dws-intent", "missing its dws-intent marker", "has 2 dws-intent markers",
"invalid marker root", "scan marker root", "duplicate intent id",
} {
if !strings.Contains(failures, want) {
t.Errorf("failure matrix missing %q:\n%s", want, failures)
}
}
if failures := validateManifest(root, routeManifest{}, nil); len(failures) == 0 {
t.Fatal("empty manifest unexpectedly passed")
}
}
func TestCrossPlatformCoverageReferenceFailureBranches(t *testing.T) {
root := t.TempDir()
if got := validateTypedContractReference(root, "../unsafe.md"); len(got) == 0 || !strings.Contains(got[0], "invalid contract_reference") {
t.Fatalf("unsafe contract result = %#v", got)
}
if got := validateTypedContractReference(root, "missing.md"); len(got) == 0 || !strings.Contains(got[0], "read typed contract") {
t.Fatalf("missing contract result = %#v", got)
}
writeTestFile(t, root, "malformed.md", "<!-- DWS_MESSAGE_RESULT_CONTRACT_END -->\n<!-- DWS_MESSAGE_RESULT_CONTRACT_START -->")
if got := strings.Join(validateTypedContractReference(root, "malformed.md"), "\n"); !strings.Contains(got, "malformed MESSAGE_RESULT") || !strings.Contains(got, "marker pair") {
t.Fatalf("malformed contract result = %s", got)
}
if got := validateEventHandoffReference(root, "../unsafe.md"); len(got) == 0 || !strings.Contains(got[0], "invalid handoff_reference") {
t.Fatalf("unsafe handoff result = %#v", got)
}
if got := validateEventHandoffReference(root, "missing-handoff.md"); len(got) == 0 || !strings.Contains(got[0], "read event handoff") {
t.Fatalf("missing handoff result = %#v", got)
}
writeTestFile(t, root, "handoff-markers.md", "no markers")
if got := strings.Join(validateEventHandoffReference(root, "handoff-markers.md"), "\n"); !strings.Contains(got, "marker pair") {
t.Fatalf("handoff marker result = %s", got)
}
if markdownCodeList(nil) != "—" || markdownBreakList(nil) != "—" {
t.Fatal("empty markdown list rendering drifted")
}
tooLong := strings.Repeat("x", 5000) + ".py"
if got := strings.Join(validateRetiredScripts(root, []string{tooLong}), "\n"); !strings.Contains(got, "inspect retired script") {
t.Fatalf("long retired script result = %s", got)
}
for _, path := range []string{"", ".", "..", "../x", "a/../b"} {
if safeRepositoryPath(path) {
t.Errorf("unsafe repository path accepted: %q", path)
}
}
if containsCLIPath("run dws chat send-more", "chat send") || !containsCLIPath("run dws chat send`", "chat send") {
t.Fatal("CLI path boundary check drifted")
}
if stringSliceContains([]string{"a/b"}, "missing") {
t.Fatal("string slice containment false positive")
}
}
func TestCrossPlatformCoverageMarkerOpenFailure(t *testing.T) {
root := t.TempDir()
writeTestFile(t, root, "refs/marker.md", "<!-- dws-intent: declared --> dws chat preferred\n")
oldOpen := markerOpen
markerOpen = func(string) (*os.File, error) { return nil, os.ErrPermission }
t.Cleanup(func() { markerOpen = oldOpen })
failures, _ := scanMarkers(root, []string{"refs"}, map[string]intentRoute{
"declared": {ID: "declared", PreferredTool: "preferred", References: []string{"refs/marker.md"}},
}, map[string]toolFact{"preferred": {PrimaryPath: "chat preferred"}})
if len(failures) == 0 || !strings.Contains(failures[0], "scan marker root") {
t.Fatalf("marker open failures = %#v", failures)
}
}
func writeTypedContractReference(t *testing.T, root, relative string) {
t.Helper()
var content strings.Builder
for _, block := range []struct {
name string
body string
}{
{name: "MESSAGE_RESULT", body: renderMessageResultContract()},
{name: "IDENTITY_CAPABILITY", body: renderIdentityCapabilityContract()},
{name: "CARD_WORKFLOW", body: renderCardWorkflowContract()},
{name: "CAPABILITY_BOUNDARY", body: renderCapabilityBoundaryContract()},
} {
content.WriteString("<!-- DWS_" + block.name + "_CONTRACT_START -->\n")
content.WriteString(block.body)
content.WriteString("\n<!-- DWS_" + block.name + "_CONTRACT_END -->\n")
}
writeTestFile(t, root, relative, content.String())
}
func writeEventHandoffReference(t *testing.T, root, relative string) {
t.Helper()
writeTestFile(t, root, relative, `<!-- DWS_EVENT_CHAT_HANDOFF_START -->
| event field | exact chat target |
|---|---|
| `+"`conversation_id`"+` | `+"`dws chat +messages-send --as user --group <conversation_id>`"+` |
| `+"`sender_open_dingtalk_id`"+` | `+"`dws chat +messages-send --as user --open-dingtalk-id <sender_open_dingtalk_id>`"+` |
<!-- DWS_EVENT_CHAT_HANDOFF_END -->
`)
}
func writeTestFile(t *testing.T, root, relative, content string) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,156 @@
{
"version": 3,
"marker_roots": [
"skills/multi/dingtalk-chat",
"skills/multi/dingtalk-event"
],
"retired_scripts": [
"skills/multi/dingtalk-chat/scripts/_chat_message_export_common.py",
"skills/multi/dingtalk-chat/scripts/chat_export_messages.py",
"skills/multi/dingtalk-chat/scripts/chat_history_with_user.py",
"skills/multi/dingtalk-chat/scripts/bot_broadcast.py",
"skills/mono/scripts/_chat_message_export_common.py",
"skills/mono/scripts/chat_export_messages.py",
"skills/mono/scripts/chat_history_with_user.py",
"skills/mono/scripts/bot_broadcast.py"
],
"contract_reference": "skills/multi/dingtalk-chat/references/contracts.md",
"handoff_reference": "skills/multi/dingtalk-event/references/event-im-output.md",
"intents": [
{
"id": "chat.send.dm",
"preferred_tool": "chat.shortcut_dm",
"forbidden_default_tools": [
"chat.send_personal_message"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
]
},
{
"id": "chat.send.group",
"preferred_tool": "chat.shortcut_send_to_group",
"forbidden_default_tools": [
"chat.send_personal_message"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
]
},
{
"id": "chat.send.advanced",
"preferred_tool": "chat.shortcut_messages_send",
"forbidden_default_tools": [
"chat.send_personal_message",
"chat.send_robot_message",
"chat.send_message_by_custom_robot",
"chat.shortcut_messages_send_by_webhook"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md",
"skills/multi/dingtalk-chat/references/chat/chat-bot.md"
]
},
{
"id": "chat.read.conversation",
"preferred_tool": "chat.shortcut_chat_messages",
"allowed_fallbacks": [
{
"tool": "chat.list_conversation_message_v2",
"reason_code": "raw_page_control"
}
],
"forbidden_default_tools": [
"chat.list_conversation_message_v2"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md",
"skills/multi/dingtalk-chat/references/chat/chat-conversation.md"
]
},
{
"id": "chat.search.cross-conversation",
"preferred_tool": "chat.shortcut_search_msg",
"allowed_fallbacks": [
{
"tool": "chat.search_messages",
"reason_code": "raw_filter_control"
}
],
"forbidden_default_tools": [
"chat.search_messages",
"chat.search_messages_by_keyword"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
]
},
{
"id": "chat.create.group",
"preferred_tool": "chat.shortcut_chat_create",
"forbidden_default_tools": [
"chat.create_group_conversation"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-group.md"
]
},
{
"id": "chat.reply.quote",
"preferred_tool": "chat.shortcut_messages_reply",
"forbidden_default_tools": [
"chat.reply_personal_message"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/01-messaging.md",
"skills/multi/dingtalk-chat/references/chat/chat-message.md"
]
},
{
"id": "chat.conversation.list-top",
"preferred_tool": "chat.shortcut_conversation_list_top",
"allowed_fallbacks": [
{
"tool": "chat.list_top_conversations",
"reason_code": "raw_response_required"
}
],
"forbidden_default_tools": [
"chat.list_top_conversations"
],
"references": [
"skills/multi/dingtalk-chat/SKILL.md",
"skills/multi/dingtalk-chat/references/chat/chat-conversation.md"
]
},
{
"id": "event.listen.im",
"preferred_tool": "event.listen_im",
"allowed_fallbacks": [
{
"tool": "event.consume",
"reason_code": "advanced_event_control"
}
],
"forbidden_default_tools": [
"event.consume"
],
"references": [
"skills/multi/dingtalk-event/SKILL.md",
"skills/multi/dingtalk-event/references/event-im.md"
]
}
]
}
+81 -48
View File
@@ -492,7 +492,7 @@ func checkToolCompatibility(toolPath string, oldTool, newTool toolSchema) []stri
}
if oldTool.Constraints != newTool.Constraints &&
!compatibleHiddenSiblingConstraintExpansion(oldTool, newTool) &&
!compatibleConstraintMemberExpansion(oldTool.Constraints, newTool.Constraints) {
!compatibleAdditiveConstraintEvolution(oldTool, newTool) {
failures = append(failures, fmt.Sprintf("schema tool %q changed constraints", toolPath))
}
if !compatiblePositionals(oldTool.Positionals, newTool.Positionals) {
@@ -514,6 +514,86 @@ func checkToolCompatibility(toolPath string, oldTool, newTool toolSchema) []stri
return failures
}
// compatibleAdditiveConstraintEvolution accepts constraint evolution that
// cannot invalidate an invocation expressible by the historical public
// parameter contract. Existing groups may only gain members; additions to a
// mutually-exclusive or require-together group must not be historical public
// parameters, because that would reject an invocation expressible by the old
// contract. Adding a member to require-one-of only loosens the group. A newly
// added mutually-exclusive group is safe when it contains at most one
// historical public parameter: aliases and newly added parameters could not
// have appeared together in an old invocation. A new require-together group is
// safe only when it contains no historical public parameter. A new
// require-one-of group always adds a requirement and is therefore incompatible.
func compatibleAdditiveConstraintEvolution(oldTool, newTool toolSchema) bool {
oldGroups, okOld := parseConstraintGroups(oldTool.Constraints)
newGroups, okNew := parseConstraintGroups(newTool.Constraints)
if !okOld || !okNew {
return false
}
for _, key := range []string{"mutually_exclusive", "require_one_of", "require_together"} {
used := make([]bool, len(newGroups[key]))
for _, oldGroup := range oldGroups[key] {
oldSet := stringSet(oldGroup)
if len(oldSet) == 0 {
return false
}
matched := false
for index, newGroup := range newGroups[key] {
newSet := stringSet(newGroup)
if used[index] || !stringSetContainsAll(newSet, oldSet) {
continue
}
if key == "mutually_exclusive" || key == "require_together" {
safe := true
for member := range newSet {
if oldSet[member] {
continue
}
if _, historical := oldTool.Parameters[member]; historical {
safe = false
break
}
}
if !safe {
continue
}
}
used[index] = true
matched = true
break
}
if !matched {
return false
}
}
for index, newGroup := range newGroups[key] {
if used[index] {
continue
}
historicalMembers := 0
for member := range stringSet(newGroup) {
if _, existed := oldTool.Parameters[member]; existed {
historicalMembers++
}
}
switch key {
case "mutually_exclusive":
if historicalMembers > 1 {
return false
}
case "require_together":
if historicalMembers > 0 {
return false
}
default: // require_one_of
return false
}
}
}
return true
}
// compatibleHiddenSiblingConstraintExpansion allows declare≡execute repairs:
// Schema may start projecting full constraint groups that include unpublished
// (hidden) execute-side siblings when the previous contract collapsed the sole
@@ -574,23 +654,6 @@ func compatibleHiddenSiblingConstraintExpansion(oldTool, newTool toolSchema) boo
return true
}
// compatibleConstraintMemberExpansion allows declare≡execute / alias-surface
// repairs that only add members to existing constraint groups. Removing a
// member, dropping a group, or adding a new group remains incompatible.
func compatibleConstraintMemberExpansion(oldConstraints, newConstraints string) bool {
oldGroups, okOld := parseConstraintGroups(oldConstraints)
newGroups, okNew := parseConstraintGroups(newConstraints)
if !okOld || !okNew {
return false
}
for _, key := range []string{"mutually_exclusive", "require_one_of", "require_together"} {
if !constraintGroupsAreMemberExpansions(oldGroups[key], newGroups[key]) {
return false
}
}
return true
}
func parseConstraintGroups(raw string) (map[string][][]string, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
@@ -611,36 +674,6 @@ func parseConstraintGroups(raw string) (map[string][][]string, bool) {
}, true
}
func constraintGroupsAreMemberExpansions(oldGroups, newGroups [][]string) bool {
if len(oldGroups) != len(newGroups) {
return false
}
used := make([]bool, len(newGroups))
for _, oldGroup := range oldGroups {
oldSet := stringSet(oldGroup)
if len(oldSet) == 0 {
return false
}
matched := false
for index, newGroup := range newGroups {
if used[index] {
continue
}
newSet := stringSet(newGroup)
if !stringSetContainsAll(newSet, oldSet) {
continue
}
used[index] = true
matched = true
break
}
if !matched {
return false
}
}
return true
}
func stringSetContainsAll(superset, subset map[string]bool) bool {
for value := range subset {
if !superset[value] {
+87 -30
View File
@@ -492,10 +492,10 @@ func TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion(t
parameter.InterfaceType = ""
})
mutateTool(&current, func(tool *toolSchema) {
tool.Constraints = `{"require_one_of":[["title","format","legacy-format"]],"mutually_exclusive":[["title","legacy-title"]]}`
tool.Constraints = `{"require_one_of":[["title","format","legacy-format"]],"mutually_exclusive":[["title","format","legacy-title"]]}`
})
// Baseline has require_one_of only; expanding with a new mutually_exclusive
// group must remain incompatible.
// Baseline has require_one_of only; a new mutually-exclusive group that
// restricts two historical public parameters must remain incompatible.
if failures := checkCompatibility(baseline, current); len(failures) == 0 {
t.Fatal("adding a new constraint group must fail compatibility")
}
@@ -532,36 +532,9 @@ func TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion(t
t.Fatalf("hidden-sibling constraint expansion should pass: %v", failures)
}
if compatibleConstraintMemberExpansion("{", `{"require_one_of":[["a"]]}`) {
t.Fatal("invalid old constraints must not expand")
}
if compatibleConstraintMemberExpansion(`{"require_one_of":[["a"]]}`, "{") {
t.Fatal("invalid new constraints must not expand")
}
if groups, ok := parseConstraintGroups(""); !ok || len(groups["require_one_of"]) != 0 {
t.Fatalf("empty constraints parse = %#v ok=%v", groups, ok)
}
if constraintGroupsAreMemberExpansions([][]string{{}}, [][]string{{"a"}}) {
t.Fatal("empty old group must not expand")
}
if constraintGroupsAreMemberExpansions(
[][]string{{"a"}, {"b"}},
[][]string{{"a", "b"}},
) {
t.Fatal("group-count reduction must not expand")
}
if constraintGroupsAreMemberExpansions(
[][]string{{"a"}, {"c"}},
[][]string{{"a", "b"}, {"x", "y"}},
) {
t.Fatal("unmatched old group must not expand")
}
if !constraintGroupsAreMemberExpansions(
[][]string{{"a"}, {"b"}},
[][]string{{"a", "b", "extra"}, {"b", "other"}},
) {
t.Fatal("used-group skip should still allow alternate match")
}
if !stringSetContainsAll(map[string]bool{"a": true, "b": true}, map[string]bool{"a": true}) ||
stringSetContainsAll(map[string]bool{"a": true}, map[string]bool{"a": true, "b": true}) {
t.Fatal("stringSetContainsAll classification is incorrect")
@@ -622,3 +595,87 @@ func TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion(t
t.Fatalf("non-empty interface_type change should fail: %v", failures)
}
}
func TestCrossPlatformCoverageSchemaCompatAdditiveConstraintEvolution(t *testing.T) {
oldTool := toolSchema{
Parameters: map[string]parameterSchema{
"target": {},
"limit": {},
},
Constraints: `{"require_one_of":[["target","legacy-target"]]}`,
}
compatible := oldTool
compatible.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"mutually_exclusive":[["target","new-target"],["new-a","new-b"]],"require_together":[["new-a","new-b"]]}`
if !compatibleAdditiveConstraintEvolution(oldTool, compatible) {
t.Fatal("member expansion and additive alias-only groups must remain compatible")
}
twoHistorical := compatible
twoHistorical.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"mutually_exclusive":[["target","limit","new-target"]]}`
if compatibleAdditiveConstraintEvolution(oldTool, twoHistorical) {
t.Fatal("new mutex group restricting two historical parameters must fail")
}
newRequirement := compatible
newRequirement.Constraints = `{"require_one_of":[["target","legacy-target","new-target"],["new-a","new-b"]]}`
if compatibleAdditiveConstraintEvolution(oldTool, newRequirement) {
t.Fatal("new require_one_of group must fail")
}
requireHistoricalTogether := compatible
requireHistoricalTogether.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"require_together":[["limit","new-limit"]]}`
if compatibleAdditiveConstraintEvolution(oldTool, requireHistoricalTogether) {
t.Fatal("new require_together group containing a historical parameter must fail")
}
removedOldGroup := compatible
removedOldGroup.Constraints = `{"mutually_exclusive":[["new-a","new-b"]]}`
if compatibleAdditiveConstraintEvolution(oldTool, removedOldGroup) {
t.Fatal("removing a historical group must fail")
}
invalid := compatible
invalid.Constraints = "{"
if compatibleAdditiveConstraintEvolution(oldTool, invalid) {
t.Fatal("invalid constraints must fail closed")
}
emptyHistoricalGroup := oldTool
emptyHistoricalGroup.Constraints = `{"require_one_of":[[]]}`
if compatibleAdditiveConstraintEvolution(emptyHistoricalGroup, compatible) {
t.Fatal("empty historical constraint group must fail closed")
}
historicalMutex := oldTool
historicalMutex.Constraints = `{"mutually_exclusive":[["target","legacy-target"]]}`
historicalMutexExpanded := oldTool
historicalMutexExpanded.Constraints = `{"mutually_exclusive":[["target","legacy-target","limit"]]}`
if compatibleAdditiveConstraintEvolution(historicalMutex, historicalMutexExpanded) {
t.Fatal("adding a historical parameter to an existing mutex group must fail")
}
historicalTogether := oldTool
historicalTogether.Constraints = `{"require_together":[["target","legacy-target"]]}`
historicalTogetherExpanded := oldTool
historicalTogetherExpanded.Constraints = `{"require_together":[["target","legacy-target","limit"]]}`
if compatibleAdditiveConstraintEvolution(historicalTogether, historicalTogetherExpanded) {
t.Fatal("adding a historical parameter to an existing require-together group must fail")
}
historicalOneOf := oldTool
historicalOneOf.Constraints = `{"require_one_of":[["target","legacy-target","limit"]]}`
if !compatibleAdditiveConstraintEvolution(oldTool, historicalOneOf) {
t.Fatal("adding a historical parameter to require-one-of only loosens the contract")
}
gateBaseline := baselineContract()
mutateTool(&gateBaseline, func(tool *toolSchema) {
tool.Parameters["folder"] = parameterSchema{Type: `"string"`}
tool.Constraints = `{"mutually_exclusive":[["title","format"]]}`
})
gateCurrent := cloneContract(gateBaseline)
mutateTool(&gateCurrent, func(tool *toolSchema) {
tool.Constraints = `{"mutually_exclusive":[["title","format","folder"]]}`
})
if failures := checkCompatibility(gateBaseline, gateCurrent); len(failures) == 0 {
t.Fatal("compatibility gate must reject an existing mutex group gaining a historical parameter")
}
multipleHistoricalGroups := oldTool
multipleHistoricalGroups.Constraints = `{"require_one_of":[["target"],["limit"]]}`
multipleExpandedGroups := oldTool
multipleExpandedGroups.Constraints = `{"require_one_of":[["target","limit"],["limit","new-limit"]]}`
if !compatibleAdditiveConstraintEvolution(multipleHistoricalGroups, multipleExpandedGroups) {
t.Fatal("each historical group must match a distinct expanded group")
}
}
+20 -2
View File
@@ -306,6 +306,23 @@ def reaction_message_count(value: Any, *, upper: bool) -> int:
return count
def primary_projection_for_message_metrics(value: Any) -> Any:
"""Select one canonical projected collection for message-level metrics.
Some stable Shortcut payloads publish compatibility aliases such as
``messages`` and ``items`` with the same rows. Walking the entire envelope
would count reactions and thread IDs once per alias and report a false
projection mismatch even though the lower and upper message sets agree.
"""
if not isinstance(value, dict):
return value
for key in ("messages", "replies", "results", "items"):
items = value.get(key)
if isinstance(items, list):
return items
return value
def thread_message_count(value: Any, *, upper: bool) -> int:
keys = {"threadId"} if upper else {
"openConvThreadId",
@@ -393,9 +410,10 @@ def summarize_capture(capture: Capture) -> dict[str, Any]:
else 0
)
exact_passthrough = bool(capture.raw_calls and upper == raw)
upper_reactions = reaction_message_count(upper, upper=True)
upper_message_projection = primary_projection_for_message_metrics(upper)
upper_reactions = reaction_message_count(upper_message_projection, upper=True)
lower_reactions = reaction_message_count(raw, upper=False)
upper_threads = thread_message_count(upper, upper=True)
upper_threads = thread_message_count(upper_message_projection, upper=True)
lower_threads = thread_message_count(raw, upper=False)
upper_pagination = pagination_meta(upper)
lower_pagination = pagination_meta(pagination_raw)
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Deterministic regression checks for the live Chat audit summarizer."""
from __future__ import annotations
from run_chat_shortcut_live_audit import (
primary_projection_for_message_metrics,
reaction_message_count,
thread_message_count,
)
def check(name: str, condition: bool) -> None:
if not condition:
raise AssertionError(f"FAIL: {name}")
print(f" ok: {name}")
def test_compatibility_aliases_are_counted_once() -> None:
rows = [{"threadId": "thread-1", "reactions": [{"type": "LIKE"}]}]
payload = {"messages": rows, "items": rows, "count": 1}
projection = primary_projection_for_message_metrics(payload)
check("messages is the canonical projection", projection is rows)
check("reaction-bearing message counted once", reaction_message_count(projection, upper=True) == 1)
check("thread-bearing message counted once", thread_message_count(projection, upper=True) == 1)
def test_projection_fallback_order() -> None:
replies = [{"threadId": "thread-2"}]
items = [{"threadId": "thread-3"}]
check(
"replies precedes items",
primary_projection_for_message_metrics({"replies": replies, "items": items}) is replies,
)
check("items remains a supported fallback", primary_projection_for_message_metrics({"items": items}) is items)
def test_non_envelope_passthrough() -> None:
rows = [{"threadId": "thread-4"}]
check("bare list passes through", primary_projection_for_message_metrics(rows) is rows)
def main() -> None:
tests = [
test_compatibility_aliases_are_counted_once,
test_projection_fallback_order,
test_non_envelope_passthrough,
]
for test in tests:
print(f"{test.__name__}:")
test()
print(f"\nAll {len(tests)} test groups passed.")
if __name__ == "__main__":
main()
+29 -9
View File
@@ -474,9 +474,22 @@ def run_live(binary: Path, timeout: int) -> list[dict[str, Any]]:
)
role_id = ""
# The platform only permits the @all and red-envelope sub-switches
# while the conversation-wide notification-off switch is enabled.
# Keep that prerequisite active across both cases, then restore it.
conversation_mute_captures = [
run_capture(
binary,
"+conversation-mute",
["--conversation-id", group_id, "--yes"],
timeout,
)
]
# Red-envelope must be exercised first. Toggling the @all sub-switch
# can make the service clear the prerequisite state for the next call.
shortcut_case(
"+conversation-mute",
"update_notification_off",
"+conversation-mute-red-envelope",
"update_red_env_notification_off",
[
["--conversation-id", group_id],
["--conversation-id", group_id, "--off"],
@@ -490,13 +503,20 @@ def run_live(binary: Path, timeout: int) -> list[dict[str, Any]]:
["--conversation-id", group_id, "--off"],
],
)
shortcut_case(
"+conversation-mute-red-envelope",
"update_red_env_notification_off",
[
["--conversation-id", group_id],
["--conversation-id", group_id, "--off"],
],
conversation_mute_captures.append(
run_capture(
binary,
"+conversation-mute",
["--conversation-id", group_id, "--off", "--yes"],
timeout,
)
)
results.append(
summarize_action(
"+conversation-mute",
"update_notification_off",
conversation_mute_captures,
)
)
shortcut_case(
"+conversation-set-top",