feat(shortcut): harden devdoc hrbrain and pat surfaces
This commit is contained in:
@@ -10,7 +10,7 @@ SCHEMA_META_INDEX_OUTPUT ?= artifacts/schema_meta_index.gob
|
||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
|
||||
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity skill-context-budget multi-im-skill-chain-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat shortcut-public-e2e-proof lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity skill-context-budget multi-im-skill-chain-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
|
||||
all: setup-hooks fmt lint build test rebuild
|
||||
|
||||
@@ -20,6 +20,7 @@ help:
|
||||
@printf " make test - Run the Go test suite\n"
|
||||
@printf " make test-plan - Verify CI test and full-suite coverage package plans cover their scopes exactly once\n"
|
||||
@printf " make test-auth-legacy-compat - Run stable legacy authentication compatibility regressions\n"
|
||||
@printf " make shortcut-public-e2e-proof - Prove every reviewed Devdoc/HRbrain/PAT public Shortcut through exact and owning raw execution\n"
|
||||
@printf " make lint - Run formatting checks, go vet, and staticcheck\n"
|
||||
@printf " make format-check - Check all repository Go source files with gofmt\n"
|
||||
@printf " make fmt - Format all repository Go source files\n"
|
||||
@@ -62,6 +63,9 @@ test-auth-legacy-compat:
|
||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||
@GO="$(GO)" $(POLICY_ENV) ./scripts/policy/check-auth-legacy-compat.sh
|
||||
|
||||
shortcut-public-e2e-proof: build
|
||||
@GO="$(GO)" DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" ./scripts/policy/check-shortcut-public-e2e-proof.sh
|
||||
|
||||
lint:
|
||||
@./scripts/dev/lint.sh
|
||||
|
||||
@@ -84,7 +88,7 @@ fmt:
|
||||
$(GO_SOURCE_LIST) > "$$go_files"; \
|
||||
xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -w -- "$$@"; fi' sh < "$$go_files"
|
||||
|
||||
policy: test-auth-legacy-compat
|
||||
policy: test-auth-legacy-compat shortcut-public-e2e-proof
|
||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||
@$(POLICY_ENV) ./scripts/policy/check-open-source-assets.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-skill-context-budget.sh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generated_at": "2026-08-21T10:07:48.251313",
|
||||
"count": 418,
|
||||
"generated_at": "2026-08-24T12:22:05.108140",
|
||||
"count": 426,
|
||||
"results": [
|
||||
{
|
||||
"suite": "semantic",
|
||||
@@ -282,6 +282,76 @@
|
||||
"semantic_delta": "更新仪表盘配置的一对一入口。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-create",
|
||||
"risk": "write",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "为指定 Base 创建数据源表并触发首次全量同步,返回新建表 ID 和同步任务 ID。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-get-config",
|
||||
"risk": "read",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "读取已有数据源表的同步配置详情(源配置、字段选择、自动同步状态)的一对一入口。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-get-fields",
|
||||
"risk": "read",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "获取指定数据源来源的可同步字段列表(字段 ID/名称/类型/是否主键),用于决定 field-ids。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-list-sources",
|
||||
"risk": "read",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "列出指定 Base 下可用的数据源条目(OA 审批模板等),提取 processCode/name/iconUrl/url 用于 sourceConfig。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-sync",
|
||||
"risk": "write",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "对已有数据源表触发手动同步(单次最多 5 张),仅触发即返回同步任务 ID。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-sync-status",
|
||||
"risk": "read",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "批量查询数据源同步任务状态(RUNNING/FINISHED/FAILED),与 sync/create/update 触发后配对使用。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
"command": "+datasource-update",
|
||||
"risk": "write",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "更新已有数据源表的同步配置并触发一次同步;不改配置可只切换 auto 开关或 field-ids。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "aitable",
|
||||
@@ -3635,6 +3705,16 @@
|
||||
"semantic_delta": "按关键字搜索可发起审批定义,严格要求显式 result 数组和稳定 processCode;已完成已知非空与保证零命中证明。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "pat",
|
||||
"command": "+browser-policy",
|
||||
"risk": "write",
|
||||
"status": "reviewed_available",
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "在隔离本地策略文件上提供显式确认、无写入请求预览、同目标磁盘读回和不暴露 agent identity 的统一结果;exact 写入与清理已通过。",
|
||||
"availability": "available"
|
||||
},
|
||||
{
|
||||
"suite": "semantic",
|
||||
"service": "report",
|
||||
@@ -4126,4 +4206,4 @@
|
||||
"availability": "available"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
publicShortcutCount = 424
|
||||
publicShortcutCount = 425
|
||||
// schemaPublishedShortcutCount counts every delivered *.shortcut_* tool,
|
||||
// including reviewed hidden compatibility and unavailable contracts.
|
||||
schemaPublishedShortcutCount = 468
|
||||
schemaPublishedShortcutCount = 482
|
||||
// publiclyDeliveredShortcutCount is the public-catalog subset of that surface.
|
||||
publiclyDeliveredShortcutCount = 424
|
||||
publiclyDeliveredShortcutCount = 425
|
||||
)
|
||||
|
||||
func TestDeliverySchemaCoversOrExactlyExcludesEveryPublicShortcutContract(t *testing.T) {
|
||||
|
||||
@@ -71,6 +71,13 @@ func patConfigDir() string {
|
||||
return filepath.Join(homeDir, ".dws")
|
||||
}
|
||||
|
||||
// BrowserPolicyConfigDir returns the exact configuration directory used by
|
||||
// the native PAT command. Shortcut adapters use this narrow seam so their
|
||||
// write/readback cycle cannot drift to a different policy file.
|
||||
func BrowserPolicyConfigDir() string {
|
||||
return patConfigDir()
|
||||
}
|
||||
|
||||
func LoadBrowserPolicy(configDir string) (*BrowserPolicy, error) {
|
||||
path := patPolicyPath(configDir)
|
||||
data, err := patPolicyReadFile(path)
|
||||
@@ -203,6 +210,32 @@ func SetBrowserPolicy(configDir, explicitAgentCode string, enabled bool) (Browse
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadStoredBrowserPolicy reads the exact default or explicitly named agent
|
||||
// entry without consulting the process agent-code environment. This is the
|
||||
// verification companion to SetBrowserPolicy: callers can prove the same
|
||||
// target was persisted rather than observing an environment-selected fallback.
|
||||
func ReadStoredBrowserPolicy(configDir, explicitAgentCode string) (BrowserPolicySelection, error) {
|
||||
agentCode, err := resolveBrowserPolicyWriteAgentCode(explicitAgentCode)
|
||||
if err != nil {
|
||||
return BrowserPolicySelection{}, err
|
||||
}
|
||||
policy, err := LoadBrowserPolicy(configDir)
|
||||
if err != nil {
|
||||
return BrowserPolicySelection{}, err
|
||||
}
|
||||
if agentCode != "" {
|
||||
entry, ok := policy.Agents[agentCode]
|
||||
if !ok {
|
||||
return BrowserPolicySelection{}, fmt.Errorf("PAT browser policy agent entry was not persisted")
|
||||
}
|
||||
return BrowserPolicySelection{Scope: "agent", AgentCode: agentCode, OpenBrowser: entry.OpenBrowser, Source: "agent"}, nil
|
||||
}
|
||||
if policy.Default == nil {
|
||||
return BrowserPolicySelection{}, fmt.Errorf("PAT browser policy default entry was not persisted")
|
||||
}
|
||||
return BrowserPolicySelection{Scope: "default", OpenBrowser: policy.Default.OpenBrowser, Source: "default"}, nil
|
||||
}
|
||||
|
||||
func newBrowserPolicyCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "browser-policy",
|
||||
|
||||
@@ -105,6 +105,9 @@ func TestCrossPlatformCoverageBrowserPolicyInjectedEdges(t *testing.T) {
|
||||
if _, err := LoadBrowserPolicy("x"); !errors.Is(err, failure) {
|
||||
t.Fatalf("read error = %v", err)
|
||||
}
|
||||
if _, err := ReadStoredBrowserPolicy("x", ""); !errors.Is(err, failure) {
|
||||
t.Fatalf("readback error = %v", err)
|
||||
}
|
||||
patPolicyReadFile = func(string) ([]byte, error) { return []byte(`{`), nil }
|
||||
if _, err := LoadBrowserPolicy("x"); err == nil {
|
||||
t.Fatal("invalid policy decoded")
|
||||
@@ -179,6 +182,53 @@ func TestCrossPlatformCoverageBrowserPolicyCommandSetError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageBrowserPolicyReadbackAndSelectionEdges(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
if got := BrowserPolicyConfigDir(); strings.TrimSpace(got) == "" {
|
||||
t.Fatal("browser policy config directory is empty")
|
||||
}
|
||||
if _, err := ReadStoredBrowserPolicy(configDir, "bad code"); err == nil {
|
||||
t.Fatal("invalid readback agent code succeeded")
|
||||
}
|
||||
if _, err := ReadStoredBrowserPolicy(configDir, "missing"); err == nil {
|
||||
t.Fatal("missing agent readback succeeded")
|
||||
}
|
||||
if _, err := ReadStoredBrowserPolicy(configDir, ""); err == nil {
|
||||
t.Fatal("missing default readback succeeded")
|
||||
}
|
||||
if _, err := SetBrowserPolicy(configDir, "agent", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agent, err := ReadStoredBrowserPolicy(configDir, "agent")
|
||||
if err != nil || agent.Scope != "agent" || agent.AgentCode != "agent" || agent.OpenBrowser {
|
||||
t.Fatalf("agent readback = %#v, %v", agent, err)
|
||||
}
|
||||
resolvedAgent, err := ResolveBrowserPolicy(configDir, "agent")
|
||||
if err != nil || resolvedAgent.Source != "agent" || resolvedAgent.OpenBrowser {
|
||||
t.Fatalf("agent selection = %#v, %v", resolvedAgent, err)
|
||||
}
|
||||
if _, err := SetBrowserPolicy(configDir, "", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultPolicy, err := ReadStoredBrowserPolicy(configDir, "")
|
||||
if err != nil || defaultPolicy.Scope != "default" || !defaultPolicy.OpenBrowser {
|
||||
t.Fatalf("default readback = %#v, %v", defaultPolicy, err)
|
||||
}
|
||||
resolvedDefault, err := ResolveBrowserPolicy(configDir, "unknown")
|
||||
if err != nil || resolvedDefault.Source != "default" || !resolvedDefault.OpenBrowser {
|
||||
t.Fatalf("default selection = %#v, %v", resolvedDefault, err)
|
||||
}
|
||||
empty, err := ResolveBrowserPolicy(t.TempDir(), "")
|
||||
if err != nil || empty.Source == "" || !empty.OpenBrowser {
|
||||
t.Fatalf("built-in selection = %#v, %v", empty, err)
|
||||
}
|
||||
|
||||
cmd := newBrowserPolicyCommand()
|
||||
if err := cmd.Execute(); err == nil {
|
||||
t.Fatal("browser policy command accepted missing --enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATPureHelperEdges(t *testing.T) {
|
||||
if got := collectChmodProductCodes([]string{"", "a,a", " b "}, []string{"b", "c"}); strings.Join(got, ",") != "a,b,c" {
|
||||
t.Fatalf("product codes = %#v", got)
|
||||
|
||||
@@ -18,6 +18,7 @@ package pat
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
@@ -91,5 +92,17 @@ DWS_CHANNEL 只用于上游 channelCode。`,
|
||||
|
||||
patCmd.AddCommand(newChmodCommand(c))
|
||||
patCmd.AddCommand(newBrowserPolicyCommand())
|
||||
// Shortcuts are mounted before this open-source PAT tree in the app. Fold
|
||||
// that pre-existing service group into the native PAT parent so Cobra and
|
||||
// Schema both see exactly one top-level path while preserving PAT's group
|
||||
// behavior and adding every +leaf alongside chmod/browser-policy.
|
||||
for _, existing := range root.Commands() {
|
||||
if existing.Name() != patCmd.Name() {
|
||||
continue
|
||||
}
|
||||
cobracmd.MergeCommandTree(patCmd, existing)
|
||||
root.RemoveCommand(existing)
|
||||
break
|
||||
}
|
||||
root.AddCommand(patCmd)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package pat
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageRegisterCommandsMergesExistingPATShortcutGroup(t *testing.T) {
|
||||
root := &cobra.Command{Use: "dws"}
|
||||
shortcutGroup := &cobra.Command{Use: "pat", Short: "pat shortcuts"}
|
||||
shortcutGroup.AddCommand(&cobra.Command{Use: "+fixture"})
|
||||
root.AddCommand(&cobra.Command{Use: "aaa-other"})
|
||||
root.AddCommand(shortcutGroup)
|
||||
|
||||
RegisterCommands(root, &fakeToolCaller{})
|
||||
|
||||
var patGroups []*cobra.Command
|
||||
for _, command := range root.Commands() {
|
||||
if command.Name() == "pat" {
|
||||
patGroups = append(patGroups, command)
|
||||
}
|
||||
}
|
||||
if len(patGroups) != 1 {
|
||||
t.Fatalf("top-level PAT groups = %d, want 1", len(patGroups))
|
||||
}
|
||||
for _, child := range []string{"+fixture", "chmod", "browser-policy"} {
|
||||
if found, _, err := patGroups[0].Find([]string{child}); err != nil || found == nil || found.Name() != child {
|
||||
t.Errorf("merged PAT child %q missing: found=%v err=%v", child, found, err)
|
||||
}
|
||||
}
|
||||
if patGroups[0].Short != "行为授权管理" {
|
||||
t.Fatalf("native PAT group metadata was not preserved: %q", patGroups[0].Short)
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,16 @@ import (
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/contact"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/devapp"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/devdoc"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/ding"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/doc"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/drive"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/hrbrain"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/live"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/mail"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/minutes"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/oa"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/pat"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/report"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/sheet"
|
||||
_ "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/smart"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package builtin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageDevdocSemanticCatalogExactlyCoversRegisteredSurface(t *testing.T) {
|
||||
raw, err := os.ReadFile("../semantic_catalog_devdoc.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var source chatSemanticCatalogFixture
|
||||
if err := json.Unmarshal(raw, &source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if source.Service != "devdoc" || len(source.Shortcuts) != 1 {
|
||||
t.Fatalf("service/count = %q/%d", source.Service, len(source.Shortcuts))
|
||||
}
|
||||
registered := map[string]shortcut.Shortcut{}
|
||||
for _, item := range shortcut.All() {
|
||||
if item.Service == "devdoc" {
|
||||
registered[item.Command] = item
|
||||
}
|
||||
}
|
||||
if len(registered) != 1 {
|
||||
t.Fatalf("registered Devdoc shortcuts = %d, want 1", len(registered))
|
||||
}
|
||||
for command, record := range source.Shortcuts {
|
||||
item, ok := registered[command]
|
||||
if !ok {
|
||||
t.Fatalf("catalog entry %s is not registered", command)
|
||||
}
|
||||
if !record.Reviewed || record.Public || record.Availability != shortcut.AvailabilityUnavailable {
|
||||
t.Fatalf("catalog entry is not reviewed private unavailable: %#v", record)
|
||||
}
|
||||
if !item.Hidden || !item.SemanticReviewed || item.Availability != shortcut.AvailabilityUnavailable || item.SemanticDelta != record.SemanticDelta {
|
||||
t.Fatalf("runtime semantic facts drifted: %#v", item)
|
||||
}
|
||||
if item.Contract.Empty() || item.Contract.Result == nil || item.OutputRollout != output.RolloutUnifiedActive || strings.TrimSpace(item.Safety.Effect) == "" {
|
||||
t.Fatal("unavailable Devdoc Shortcut lacks Contract/Result/Safety/unified output")
|
||||
}
|
||||
if item.Contract.Interface == nil || item.Contract.Interface.Availability != "unavailable" || !strings.Contains(string(item.Contract.Result.DataSchema), "zeroMatchProven") {
|
||||
t.Fatal("unavailable Devdoc Shortcut does not publish the semantic zero-match boundary")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package builtin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainSemanticCatalogExactlyCoversRegisteredSurface(t *testing.T) {
|
||||
raw, err := os.ReadFile("../semantic_catalog_hrbrain.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var source chatSemanticCatalogFixture
|
||||
if err := json.Unmarshal(raw, &source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registered := map[string]shortcut.Shortcut{}
|
||||
for _, item := range shortcut.All() {
|
||||
if item.Service == "hrbrain" {
|
||||
registered[item.Command] = item
|
||||
}
|
||||
}
|
||||
if source.Service != "hrbrain" || len(source.Shortcuts) != 11 || len(registered) != 11 {
|
||||
t.Fatalf("service/catalog/registered = %q/%d/%d", source.Service, len(source.Shortcuts), len(registered))
|
||||
}
|
||||
var missing, stale []string
|
||||
blockerCounts := map[string]int{}
|
||||
for command, item := range registered {
|
||||
record, ok := source.Shortcuts[command]
|
||||
if !ok {
|
||||
missing = append(missing, command)
|
||||
continue
|
||||
}
|
||||
if record.Public || record.Availability != "" || !record.Reviewed || !item.SemanticReviewed || item.Availability != shortcut.AvailabilityUnavailable || !item.Hidden {
|
||||
t.Errorf("%s availability/review drift", command)
|
||||
}
|
||||
if strings.TrimSpace(item.SemanticDelta) == "" || item.SemanticDelta != record.SemanticDelta {
|
||||
t.Errorf("%s semantic delta drift", command)
|
||||
}
|
||||
if item.OutputRollout != output.RolloutUnifiedActive || item.Contract.Empty() || item.Contract.Result == nil || strings.TrimSpace(item.Safety.Effect) == "" {
|
||||
t.Errorf("%s lacks Contract/Result/Safety/unified output", command)
|
||||
}
|
||||
if item.Contract.Interface == nil || strings.Contains(item.Contract.Interface.Reason, "shortcut_defect") {
|
||||
t.Errorf("%s lacks a reviewed non-Shortcut blocker", command)
|
||||
} else if strings.Contains(item.Contract.Interface.Reason, "classified=adapter_business_service") {
|
||||
blockerCounts["adapter_business_service"]++
|
||||
} else if strings.Contains(item.Contract.Interface.Reason, "classified=tenant_fixture") {
|
||||
blockerCounts["tenant_fixture"]++
|
||||
} else {
|
||||
t.Errorf("%s has unknown blocker classification", command)
|
||||
}
|
||||
}
|
||||
for command := range source.Shortcuts {
|
||||
if _, ok := registered[command]; !ok {
|
||||
stale = append(stale, command)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
sort.Strings(stale)
|
||||
if len(missing) > 0 || len(stale) > 0 {
|
||||
t.Fatalf("catalog mismatch: missing=%v stale=%v", missing, stale)
|
||||
}
|
||||
if blockerCounts["adapter_business_service"] != 9 || blockerCounts["tenant_fixture"] != 2 {
|
||||
t.Fatalf("blocker counts = %#v", blockerCounts)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package builtin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoveragePATSemanticCatalogExactlyCoversRegisteredSurface(t *testing.T) {
|
||||
raw, err := os.ReadFile("../semantic_catalog_pat.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var source chatSemanticCatalogFixture
|
||||
if err := json.Unmarshal(raw, &source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registered := map[string]shortcut.Shortcut{}
|
||||
for _, item := range shortcut.All() {
|
||||
if item.Service == "pat" {
|
||||
registered[item.Command] = item
|
||||
}
|
||||
}
|
||||
if source.Service != "pat" || len(source.Shortcuts) != 2 || len(registered) != 2 {
|
||||
t.Fatalf("service/catalog/registered = %q/%d/%d", source.Service, len(source.Shortcuts), len(registered))
|
||||
}
|
||||
public := 0
|
||||
for command, record := range source.Shortcuts {
|
||||
item, ok := registered[command]
|
||||
if !ok || !record.Reviewed || !item.SemanticReviewed || strings.TrimSpace(item.SemanticDelta) == "" || item.SemanticDelta != record.SemanticDelta {
|
||||
t.Fatalf("%s semantic review drift", command)
|
||||
}
|
||||
if record.Public {
|
||||
public++
|
||||
if item.Hidden || item.Availability != shortcut.AvailabilityAvailable || item.OutputRollout != output.RolloutUnifiedActive || item.Contract.Result == nil || strings.TrimSpace(item.Safety.Effect) == "" {
|
||||
t.Fatalf("%s public contract drift", command)
|
||||
}
|
||||
} else if !item.Hidden || item.Availability != shortcut.AvailabilityUnavailable {
|
||||
t.Fatalf("%s unavailable visibility drift", command)
|
||||
}
|
||||
if command == "+authorize" {
|
||||
if record.Disposition != shortcut.DispositionAliasInternal || record.Primary != "pat chmod" || item.PrimaryCommand != "pat chmod" || !strings.Contains(item.SemanticDelta, "classified=routed") {
|
||||
t.Fatalf("%s must route to the reviewed pat chmod execution chain", command)
|
||||
}
|
||||
if item.Contract.Interface == nil || !strings.Contains(item.Contract.Interface.Reason, "blocked_fixture") || !strings.Contains(item.Contract.Interface.Reason, "classified=routed") {
|
||||
t.Fatalf("%s must separate routed implementation from unproved live write", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
if public != 1 {
|
||||
t.Fatalf("public PAT shortcuts = %d, want 1", public)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
// Package devdoc registers strict Shortcut adapters for DingTalk Open Platform
|
||||
// documentation discovery.
|
||||
package devdoc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/responsecheck"
|
||||
)
|
||||
|
||||
const devdocCompositeReason = "Unavailable after exact Shortcut plus raw isolation: the downstream interface performs semantic recall and returned nonempty neighbors for runtime-random zero-match candidates, so no guaranteed zero-match query can satisfy the public list/search proof contract."
|
||||
|
||||
type devdocPageEvidence struct {
|
||||
CurrentPage int
|
||||
PageSize int
|
||||
TotalCount int
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
func devdocSearchResult() *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{"type":"object","description":"严格校验但未满足公开 zero-match 证明的语义文档搜索页","properties":{"count":{"type":"integer","description":"当前页文档数量"},"documents":{"type":"array","description":"当前页开放平台文档","items":{"type":"object","description":"带稳定 URL 的开放平台文档","properties":{"title":{"type":"string","description":"文档标题"},"url":{"type":"string","description":"文档稳定 URL"},"description":{"type":"string","description":"文档摘要"}},"required":["title","url"],"additionalProperties":false}},"currentPage":{"type":"integer","description":"服务端确认的当前页码"},"pageSize":{"type":"integer","description":"服务端确认的分页大小"},"totalCount":{"type":"integer","description":"服务端报告的语义召回总数"},"complete":{"type":"boolean","description":"是否到达语义召回末页;不表示 query zero-match 已证明"},"matchSemantics":{"type":"string","description":"下游搜索匹配语义","enum":["semantic"]},"zeroMatchProven":{"type":"boolean","description":"是否通过保证零匹配 query 证明公开投影;当前固定为 false"}},"required":["count","documents","currentPage","pageSize","totalCount","complete","matchSemantics","zeroMatchProven"],"additionalProperties":false}`),
|
||||
}
|
||||
}
|
||||
|
||||
func devdocPagination() *contract.PaginationSpec {
|
||||
return &contract.PaginationSpec{
|
||||
Kind: contract.PaginationKindCursor,
|
||||
CursorParameter: "page",
|
||||
MetaPath: contract.PaginationMetaPath,
|
||||
EndpointExhaustedPath: contract.PaginationExhaustedPath,
|
||||
NextTokenPath: contract.PaginationNextTokenPath,
|
||||
}
|
||||
}
|
||||
|
||||
var SearchDocs = shortcut.Shortcut{
|
||||
OutputRollout: output.RolloutUnifiedActive,
|
||||
Service: "devdoc",
|
||||
Command: "+search-docs",
|
||||
Product: "devdoc",
|
||||
Description: "审计钉钉开放平台语义文档搜索结果",
|
||||
Intent: "需要语义搜索钉钉开放平台文档且明确接受无法证明 query zero-match 时才使用;当前 Shortcut 保持 unavailable,请路由到 raw `devdoc article search`。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Hidden: true,
|
||||
Availability: shortcut.AvailabilityUnavailable,
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent",
|
||||
},
|
||||
Contract: corecmd.ContractDecl{
|
||||
Identity: contract.ToolIdentitySpec{
|
||||
ProductID: "devdoc", Name: "shortcut_search_docs", CanonicalPath: "devdoc.shortcut_search_docs",
|
||||
CLIPath: "devdoc +search-docs", PrimaryCLIPath: "devdoc +search-docs",
|
||||
},
|
||||
Description: "审计钉钉开放平台语义文档搜索结果",
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceUnavailable, Reason: devdocCompositeReason,
|
||||
},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "审计钉钉开放平台语义文档搜索结果",
|
||||
UseWhen: []string{"需要语义搜索钉钉开放平台文档且明确接受无法证明 query zero-match 时才使用;当前 Shortcut 保持 unavailable,请路由到 raw `devdoc article search`。"},
|
||||
AvoidWhen: []string{"需要精确匹配或保证零命中时不要使用;搜索用户业务文档时使用 drive/wiki/doc"},
|
||||
Examples: []string{`dws devdoc +search-docs --query "OAuth2 接入" --size 10 --format json`},
|
||||
},
|
||||
Parameters: []contract.ParamDecl{
|
||||
{Name: "query", Property: "keyword"},
|
||||
{Name: "page", Property: "page", InterfaceType: "number"},
|
||||
{Name: "size", Property: "size", InterfaceType: "number"},
|
||||
},
|
||||
Result: devdocSearchResult(),
|
||||
Pagination: devdocPagination(),
|
||||
},
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "query", Type: shortcut.FlagString, Required: true, Desc: "搜索关键词去除空白后必须非空"},
|
||||
{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码必须大于 0"},
|
||||
{Name: "size", Type: shortcut.FlagInt, Default: "10", Desc: "每页数量必须在 1 到 50 之间"},
|
||||
},
|
||||
Constraints: []shortcut.Constraint{
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"query"}, Description: "搜索关键词去除空白后必须非空"},
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"page"}, Description: "页码必须大于 0"},
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"size"}, Description: "每页数量必须在 1 到 50 之间"},
|
||||
},
|
||||
Tips: []string{`dws devdoc +search-docs --query "OAuth2 接入" --size 10 --format json`},
|
||||
Validate: func(rt *shortcut.RuntimeContext) error {
|
||||
if strings.TrimSpace(rt.Str("query")) == "" {
|
||||
return apperrors.NewValidation("--query 不能为空")
|
||||
}
|
||||
if rt.Int("page") < 1 {
|
||||
return apperrors.NewValidation("--page 必须大于 0")
|
||||
}
|
||||
if size := rt.Int("size"); size < 1 || size > 50 {
|
||||
return apperrors.NewValidation("--size 必须在 1 到 50 之间")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Execute: executeSearchDocs,
|
||||
}
|
||||
|
||||
func executeSearchDocs(rt *shortcut.RuntimeContext) error {
|
||||
page, size := rt.Int("page"), rt.Int("size")
|
||||
data, err := rt.CallMCPData("devdoc", "search_open_platform_docs", map[string]any{
|
||||
"keyword": strings.TrimSpace(rt.Str("query")),
|
||||
"page": page,
|
||||
"size": size,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
documents, evidence, err := devdocProjectSearch(data, page, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"count": len(documents),
|
||||
"documents": documents,
|
||||
"currentPage": evidence.CurrentPage,
|
||||
"pageSize": evidence.PageSize,
|
||||
"totalCount": evidence.TotalCount,
|
||||
"complete": !evidence.HasMore,
|
||||
"matchSemantics": "semantic",
|
||||
"zeroMatchProven": false,
|
||||
}
|
||||
next := ""
|
||||
if evidence.HasMore {
|
||||
next = strconv.Itoa(evidence.CurrentPage + 1)
|
||||
}
|
||||
// Strict page evidence guarantees the only two valid framework states:
|
||||
// terminal+empty-next or continuing+positive next page.
|
||||
pagination, _ := output.NewPagination(!evidence.HasMore, next)
|
||||
meta := &output.Meta{Count: output.NewCount(len(documents)), Pagination: pagination}
|
||||
return output.StoreResult(rt.Command().Context(), output.Success(payload, output.WithMeta(meta)))
|
||||
}
|
||||
|
||||
func devdocProjectSearch(data map[string]any, requestedPage, requestedSize int) ([]map[string]any, devdocPageEvidence, error) {
|
||||
const operation = "devdoc/search_open_platform_docs"
|
||||
items, err := responsecheck.RequireObjectCollection(data, operation, "result.items")
|
||||
if err != nil {
|
||||
return nil, devdocPageEvidence{}, err
|
||||
}
|
||||
// RequireObjectCollection already proved that result is an object while
|
||||
// resolving result.items, so a second fallible shape check is unreachable.
|
||||
result, _ := data["result"].(map[string]any)
|
||||
evidence, err := devdocParsePage(result, requestedPage, requestedSize, len(items))
|
||||
if err != nil {
|
||||
return nil, devdocPageEvidence{}, err
|
||||
}
|
||||
documents := make([]map[string]any, 0, len(items))
|
||||
seenURLs := make(map[string]struct{}, len(items))
|
||||
for index, item := range items {
|
||||
title, titleOK := devdocNonEmptyString(item["title"])
|
||||
url, urlOK := devdocNonEmptyString(item["url"])
|
||||
if !titleOK || !urlOK {
|
||||
return nil, devdocPageEvidence{}, responsecheck.Error(operation, "malformed_item", fmt.Sprintf("文档结果第 %d 项缺少非空 title 或 url", index))
|
||||
}
|
||||
if _, duplicate := seenURLs[url]; duplicate {
|
||||
return nil, devdocPageEvidence{}, responsecheck.Error(operation, "duplicate_item_identity", fmt.Sprintf("文档结果第 %d 项 URL 重复", index))
|
||||
}
|
||||
seenURLs[url] = struct{}{}
|
||||
document := map[string]any{"title": title, "url": url}
|
||||
if description, ok := devdocOptionalString(item["desc"]); ok {
|
||||
document["description"] = description
|
||||
}
|
||||
documents = append(documents, document)
|
||||
}
|
||||
return documents, evidence, nil
|
||||
}
|
||||
|
||||
func devdocParsePage(result map[string]any, requestedPage, requestedSize, itemCount int) (devdocPageEvidence, error) {
|
||||
const operation = "devdoc/search_open_platform_docs"
|
||||
currentPage, ok := devdocInteger(result["currentPage"])
|
||||
if !ok || currentPage < 1 || currentPage != requestedPage {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "pagination_page_mismatch", "响应 currentPage 缺失、错型或与请求页码不一致")
|
||||
}
|
||||
pageSize, ok := devdocInteger(result["pageSize"])
|
||||
if !ok || pageSize < 1 || pageSize != requestedSize {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "pagination_size_mismatch", "响应 pageSize 缺失、错型或与请求分页大小不一致")
|
||||
}
|
||||
totalCount, ok := devdocInteger(result["totalCount"])
|
||||
if !ok || totalCount < 0 || totalCount < itemCount {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "invalid_total_count", "响应 totalCount 缺失、错型、为负数或小于当前页条数")
|
||||
}
|
||||
if itemCount > pageSize {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "page_size_exceeded", "当前页结果数量超过服务端 pageSize")
|
||||
}
|
||||
hasMore, ok := result["hasMore"].(bool)
|
||||
if !ok {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "malformed_pagination", "响应 hasMore 缺失或不是布尔值")
|
||||
}
|
||||
pageEnd := currentPage * pageSize
|
||||
if hasMore && pageEnd >= totalCount {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "conflicting_pagination", "hasMore=true 但 currentPage/pageSize/totalCount 已到达末页")
|
||||
}
|
||||
if !hasMore && pageEnd < totalCount {
|
||||
return devdocPageEvidence{}, responsecheck.Error(operation, "conflicting_pagination", "hasMore=false 但 totalCount 表明仍有下一页")
|
||||
}
|
||||
return devdocPageEvidence{CurrentPage: currentPage, PageSize: pageSize, TotalCount: totalCount, HasMore: hasMore}, nil
|
||||
}
|
||||
|
||||
func devdocInteger(value any) (int, bool) {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
if math.IsNaN(typed) || math.IsInf(typed, 0) || typed != math.Trunc(typed) || typed > float64(math.MaxInt) {
|
||||
return 0, false
|
||||
}
|
||||
return int(typed), true
|
||||
case int:
|
||||
return typed, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func devdocNonEmptyString(value any) (string, bool) {
|
||||
text, ok := value.(string)
|
||||
text = strings.TrimSpace(text)
|
||||
return text, ok && text != ""
|
||||
}
|
||||
|
||||
func devdocOptionalString(value any) (string, bool) {
|
||||
if value == nil {
|
||||
return "", false
|
||||
}
|
||||
return devdocNonEmptyString(value)
|
||||
}
|
||||
|
||||
func init() {
|
||||
shortcut.Register(SearchDocs)
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package devdoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type devdocCoverageCaller struct {
|
||||
response string
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (caller *devdocCoverageCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
||||
caller.calls++
|
||||
if caller.err != nil {
|
||||
return nil, caller.err
|
||||
}
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: caller.response}}}, nil
|
||||
}
|
||||
func (*devdocCoverageCaller) Format() string { return "json" }
|
||||
func (*devdocCoverageCaller) DryRun() bool { return false }
|
||||
func (*devdocCoverageCaller) Fields() string { return "" }
|
||||
func (*devdocCoverageCaller) JQ() string { return "" }
|
||||
|
||||
func runDevdocCoverage(t *testing.T, caller *devdocCoverageCaller, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
helpers.InitDepsForTest(t, caller)
|
||||
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
||||
root.PersistentFlags().Bool("yes", false, "")
|
||||
root.PersistentFlags().Bool("dry-run", false, "")
|
||||
root.PersistentFlags().String("format", "json", "")
|
||||
service := &cobra.Command{Use: "devdoc"}
|
||||
service.AddCommand(corecmd.New(shortcut.FromShortcut(SearchDocs)))
|
||||
root.AddCommand(service)
|
||||
ctx, _ := output.WithResultStore(context.Background())
|
||||
root.SetContext(ctx)
|
||||
var stdout bytes.Buffer
|
||||
root.SetOut(&stdout)
|
||||
root.SetErr(io.Discard)
|
||||
root.SetArgs(append([]string{"devdoc", SearchDocs.Command}, args...))
|
||||
executed, err := root.ExecuteC()
|
||||
if err == nil {
|
||||
_, _, err = output.EmitStoredResult(executed)
|
||||
}
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDevdocUnavailableContractIsUnifiedTypedAndTruthful(t *testing.T) {
|
||||
if SearchDocs.OutputRollout != output.RolloutUnifiedActive {
|
||||
t.Fatalf("rollout = %q", SearchDocs.OutputRollout)
|
||||
}
|
||||
if !SearchDocs.Hidden || SearchDocs.Availability != "unavailable" || SearchDocs.Contract.Interface == nil || SearchDocs.Contract.Interface.Availability != "unavailable" {
|
||||
t.Fatal("Devdoc semantic search must remain hidden/unavailable until a guaranteed zero-match query is provable")
|
||||
}
|
||||
if SearchDocs.Contract.Empty() || SearchDocs.Contract.Result == nil || SearchDocs.Contract.Pagination == nil {
|
||||
t.Fatal("Devdoc Shortcut must retain Contract, Result, and Pagination for audit")
|
||||
}
|
||||
resultSchema := string(SearchDocs.Contract.Result.DataSchema)
|
||||
if !strings.Contains(resultSchema, "matchSemantics") || !strings.Contains(resultSchema, "zeroMatchProven") || strings.TrimSpace(SearchDocs.Safety.Effect) == "" {
|
||||
t.Fatal("Devdoc Result must declare semantic matching and the unproven zero-match boundary")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDevdocSearchRejectsBrokenResponses(t *testing.T) {
|
||||
explicitEmpty := map[string]any{
|
||||
"success": true,
|
||||
"result": map[string]any{
|
||||
"items": []any{}, "currentPage": float64(3), "pageSize": float64(10),
|
||||
"totalCount": float64(2), "hasMore": false,
|
||||
},
|
||||
}
|
||||
documents, page, err := devdocProjectSearch(explicitEmpty, 3, 10)
|
||||
if err != nil || len(documents) != 0 || page.HasMore {
|
||||
t.Fatalf("explicit empty page: documents=%v page=%+v err=%v", documents, page, err)
|
||||
}
|
||||
// This proves only that an explicitly empty terminal page is parsed
|
||||
// truthfully. It is deliberately not evidence for a zero-match query.
|
||||
|
||||
broken := map[string]map[string]any{
|
||||
"empty response": {},
|
||||
"missing success": {"result": map[string]any{"items": []any{}}},
|
||||
"success false": {"success": false, "result": map[string]any{"items": []any{}}},
|
||||
"wrong success type": {"success": "true", "result": map[string]any{"items": []any{}}},
|
||||
"missing collection": {"success": true, "result": map[string]any{"hasMore": false}},
|
||||
"wrong collection type": {"success": true, "result": map[string]any{"items": map[string]any{}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(0), "hasMore": false}},
|
||||
"bad item": {"success": true, "result": map[string]any{"items": []any{"bad"}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(1), "hasMore": false}},
|
||||
"missing item identity": {"success": true, "result": map[string]any{"items": []any{map[string]any{"title": "doc"}}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(1), "hasMore": false}},
|
||||
"duplicate identity": {"success": true, "result": map[string]any{"items": []any{map[string]any{"title": "one", "url": "https://example.invalid/doc"}, map[string]any{"title": "two", "url": "https://example.invalid/doc"}}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(2), "hasMore": false}},
|
||||
"missing pagination": {"success": true, "result": map[string]any{"items": []any{}}},
|
||||
"mismatched page": {"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(2), "pageSize": float64(10), "totalCount": float64(0), "hasMore": false}},
|
||||
"wrong hasMore type": {"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(0), "hasMore": "false"}},
|
||||
"false completion": {"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(11), "hasMore": false}},
|
||||
"false continuation": {"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(10), "totalCount": float64(10), "hasMore": true}},
|
||||
}
|
||||
for name, payload := range broken {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got, _, err := devdocProjectSearch(payload, 1, 10); err == nil {
|
||||
t.Fatalf("broken response accepted: %#v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDevdocSearchProjectsKnownNonemptyPage(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"success": true,
|
||||
"result": map[string]any{
|
||||
"items": []any{
|
||||
map[string]any{"title": "Authentication", "url": "https://example.invalid/auth", "desc": "Guide"},
|
||||
map[string]any{"title": "Errors", "url": "https://example.invalid/errors", "desc": nil},
|
||||
},
|
||||
"currentPage": float64(1), "pageSize": float64(2), "totalCount": float64(3), "hasMore": true,
|
||||
},
|
||||
}
|
||||
documents, page, err := devdocProjectSearch(payload, 1, 2)
|
||||
if err != nil || len(documents) != 2 || !page.HasMore {
|
||||
t.Fatalf("documents=%#v page=%+v err=%v", documents, page, err)
|
||||
}
|
||||
if documents[0]["description"] != "Guide" {
|
||||
t.Fatalf("first document=%#v", documents[0])
|
||||
}
|
||||
if _, exists := documents[1]["description"]; exists {
|
||||
t.Fatalf("nil description must be omitted: %#v", documents[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDevdocExecuteValidationAndPagination(t *testing.T) {
|
||||
blankCommand := corecmd.New(shortcut.FromShortcut(SearchDocs))
|
||||
if err := blankCommand.Flags().Set("query", " "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SearchDocs.Validate(shortcut.RuntimeContextForTest(blankCommand, SearchDocs)); err == nil {
|
||||
t.Fatal("blank query validation succeeded")
|
||||
}
|
||||
for name, args := range map[string][]string{
|
||||
"blank query": {"--query", " "},
|
||||
"zero page": {"--query", "query", "--page", "0"},
|
||||
"zero size": {"--query", "query", "--size", "0"},
|
||||
"large size": {"--query", "query", "--size", "51"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
caller := &devdocCoverageCaller{}
|
||||
if _, err := runDevdocCoverage(t, caller, args...); err == nil {
|
||||
t.Fatal("invalid input succeeded")
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("remote calls before validation = %d", caller.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for name, response := range map[string]string{
|
||||
"terminal": `{"success":true,"result":{"items":[{"title":"Doc","url":"https://example.invalid/doc"}],"currentPage":1,"pageSize":10,"totalCount":1,"hasMore":false}}`,
|
||||
"continuing": `{"success":true,"result":{"items":[{"title":"Doc","url":"https://example.invalid/doc"}],"currentPage":1,"pageSize":1,"totalCount":2,"hasMore":true}}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
caller := &devdocCoverageCaller{response: response}
|
||||
args := []string{"--query", " query "}
|
||||
if name == "continuing" {
|
||||
args = append(args, "--size", "1")
|
||||
}
|
||||
if _, err := runDevdocCoverage(t, caller, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if caller.calls != 1 {
|
||||
t.Fatalf("calls = %d", caller.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
caller := &devdocCoverageCaller{err: errors.New("transport")}
|
||||
if _, err := runDevdocCoverage(t, caller, "--query", "query"); err == nil {
|
||||
t.Fatal("transport failure succeeded")
|
||||
}
|
||||
malformed := &devdocCoverageCaller{response: `{"success":true,"result":{"items":"bad"}}`}
|
||||
if _, err := runDevdocCoverage(t, malformed, "--query", "query"); err == nil {
|
||||
t.Fatal("malformed projection succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDevdocPageAndScalarEdges(t *testing.T) {
|
||||
base := func() map[string]any {
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"result": map[string]any{
|
||||
"items": []any{}, "currentPage": float64(1), "pageSize": float64(2),
|
||||
"totalCount": float64(0), "hasMore": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
for name, mutate := range map[string]func(map[string]any){
|
||||
"page size mismatch": func(result map[string]any) { result["pageSize"] = float64(3) },
|
||||
"negative total": func(result map[string]any) { result["totalCount"] = float64(-1) },
|
||||
"total below items": func(result map[string]any) {
|
||||
result["items"] = []any{map[string]any{"title": "one", "url": "https://example.invalid/one"}}
|
||||
result["totalCount"] = float64(0)
|
||||
},
|
||||
"items exceed size": func(result map[string]any) {
|
||||
result["items"] = []any{
|
||||
map[string]any{"title": "one", "url": "https://example.invalid/one"},
|
||||
map[string]any{"title": "two", "url": "https://example.invalid/two"},
|
||||
map[string]any{"title": "three", "url": "https://example.invalid/three"},
|
||||
}
|
||||
result["totalCount"] = float64(3)
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
payload := base()
|
||||
mutate(payload["result"].(map[string]any))
|
||||
if _, _, err := devdocProjectSearch(payload, 1, 2); err == nil {
|
||||
t.Fatal("invalid page succeeded")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, value := range []any{math.NaN(), math.Inf(1), 1.5, float64(math.MaxInt) * 2, "1"} {
|
||||
if _, ok := devdocInteger(value); ok {
|
||||
t.Fatalf("invalid integer accepted: %#v", value)
|
||||
}
|
||||
}
|
||||
if got, ok := devdocInteger(7); !ok || got != 7 {
|
||||
t.Fatalf("int projection = %d, %v", got, ok)
|
||||
}
|
||||
if _, ok := devdocOptionalString(7); ok {
|
||||
t.Fatal("non-string optional value accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
// Package hrbrain registers fail-closed Shortcut adapters for DingTalk
|
||||
// Organization Brain reads. Every declaration remains unavailable until the
|
||||
// lower service supplies a verifiable non-null result and safe live fixtures.
|
||||
package hrbrain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/responsecheck"
|
||||
)
|
||||
|
||||
const (
|
||||
hrbrainBlockerAdapterBusiness = "adapter_business_service"
|
||||
hrbrainBlockerTenantFixture = "tenant_fixture"
|
||||
)
|
||||
|
||||
func hrbrainUnavailableReason(command string) string {
|
||||
switch command {
|
||||
case "+get-pool", "+list-pool-employees":
|
||||
return "classified=" + hrbrainBlockerTenantFixture + "; exact Shortcut and raw atomic calls reach the same remote operation and upstream error, but no safe nonempty pool fixture exists to prove identity and pagination."
|
||||
case "+list-pools", "+profile-labels":
|
||||
return "classified=" + hrbrainBlockerAdapterBusiness + "; raw atomic calls return success=true with result=null and the exact Shortcut rejects the response, so the business response contract cannot prove a collection or legitimate empty result."
|
||||
case "+profile-metadata", "+query-profile", "+profile-career", "+profile-performance", "+search-employees", "+search-employees-structured", "+search-fields":
|
||||
return "classified=" + hrbrainBlockerAdapterBusiness + "; exact Shortcut and raw atomic calls reach the same remote server, operation, and upstream error, so no Shortcut-layer defect can be repaired without downstream capability or business-service recovery."
|
||||
default:
|
||||
panic("missing reviewed HRbrain blocker classification for " + command)
|
||||
}
|
||||
}
|
||||
|
||||
type hrbrainPage struct {
|
||||
CurrentPage int
|
||||
PageSize int
|
||||
TotalCount int
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
func hrbrainReadSafety() contract.SafetySpec {
|
||||
return contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}
|
||||
}
|
||||
|
||||
func hrbrainObjectResult(description string) *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(fmt.Sprintf(
|
||||
`{"type":"object","description":%q,"properties":{"value":{"type":"object","description":"严格校验且身份匹配的 HRbrain 业务对象","additionalProperties":true}},"required":["value"],"additionalProperties":false}`,
|
||||
description,
|
||||
)),
|
||||
SensitivePaths: []string{"value.name", "value.mobile", "value.email", "value.workNo", "value.userId"},
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainCollectionResult(description string) *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(fmt.Sprintf(
|
||||
`{"type":"object","description":%q,"properties":{"count":{"type":"integer","description":"当前响应中的有效记录数量"},"items":{"type":"array","description":%q,"items":{"type":"object","description":"严格校验且带稳定身份的 HRbrain 业务记录","additionalProperties":true}}},"required":["count","items"],"additionalProperties":false}`,
|
||||
description, description,
|
||||
)),
|
||||
SensitivePaths: []string{"items.name", "items.mobile", "items.email", "items.workNo", "items.userId"},
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainPageResult(description string) *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(fmt.Sprintf(
|
||||
`{"type":"object","description":%q,"properties":{"count":{"type":"integer","description":"当前页有效记录数量"},"items":{"type":"array","description":%q,"items":{"type":"object","description":"严格校验且带稳定身份的 HRbrain 业务记录","additionalProperties":true}},"currentPage":{"type":"integer","description":"服务端确认的当前页码"},"pageSize":{"type":"integer","description":"服务端确认的分页大小"},"totalCount":{"type":"integer","description":"服务端报告的总记录数"},"complete":{"type":"boolean","description":"服务端证据是否证明到达末页"}},"required":["count","items","currentPage","pageSize","totalCount","complete"],"additionalProperties":false}`,
|
||||
description, description,
|
||||
)),
|
||||
SensitivePaths: []string{"items.name", "items.mobile", "items.email", "items.workNo", "items.userId"},
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainPagination() *contract.PaginationSpec {
|
||||
return &contract.PaginationSpec{
|
||||
Kind: contract.PaginationKindCursor, CursorParameter: "page",
|
||||
MetaPath: contract.PaginationMetaPath, EndpointExhaustedPath: contract.PaginationExhaustedPath,
|
||||
NextTokenPath: contract.PaginationNextTokenPath,
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainContract(command, description, intent string, result *contract.ResultSpec, pagination *contract.PaginationSpec, params []contract.ParamDecl, examples ...string) corecmd.ContractDecl {
|
||||
name := "shortcut_" + strings.ReplaceAll(strings.TrimPrefix(command, "+"), "-", "_")
|
||||
path := "hrbrain " + command
|
||||
return corecmd.ContractDecl{
|
||||
Identity: contract.ToolIdentitySpec{
|
||||
ProductID: "hrbrain", Name: name, CanonicalPath: "hrbrain." + name,
|
||||
CLIPath: path, PrimaryCLIPath: path,
|
||||
},
|
||||
Description: description,
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceUnavailable, Reason: hrbrainUnavailableReason(command),
|
||||
},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: description,
|
||||
UseWhen: []string{intent},
|
||||
AvoidWhen: []string{"该 HRbrain Shortcut 当前 unavailable;不要把 null、业务错误或缺少安全 fixture 当作成功,基础通讯录查询改用 contact"},
|
||||
Examples: examples,
|
||||
},
|
||||
Parameters: params,
|
||||
Result: result,
|
||||
Pagination: pagination,
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainBase(command, description, intent string, result *contract.ResultSpec, pagination *contract.PaginationSpec, flags []shortcut.Flag, params []contract.ParamDecl, examples ...string) shortcut.Shortcut {
|
||||
return shortcut.Shortcut{
|
||||
OutputRollout: output.RolloutUnifiedActive,
|
||||
Service: "hrbrain",
|
||||
Command: command,
|
||||
Product: "hrbrain",
|
||||
Description: description,
|
||||
Intent: intent,
|
||||
Risk: shortcut.RiskRead,
|
||||
Safety: hrbrainReadSafety(),
|
||||
Contract: hrbrainContract(command, description, intent, result, pagination, params, examples...),
|
||||
Flags: flags,
|
||||
Tips: examples,
|
||||
Hidden: true,
|
||||
Availability: shortcut.AvailabilityUnavailable,
|
||||
}
|
||||
}
|
||||
|
||||
var ListPools = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+list-pools", "查询组织大脑人才池列表", "需要按名称、类型、创建人或标签发现人才池时使用;当前必须保持 unavailable,直到下游返回可验证分页数组。",
|
||||
hrbrainPageResult("严格校验的人才池搜索页"), hrbrainPagination(),
|
||||
[]shortcut.Flag{
|
||||
{Name: "keyword", Type: shortcut.FlagString, Desc: "人才池名称关键词"},
|
||||
{Name: "pool-type", Type: shortcut.FlagString, Desc: "人才池类型"},
|
||||
{Name: "creator", Type: shortcut.FlagString, Desc: "创建人稳定身份"},
|
||||
{Name: "labels", Type: shortcut.FlagStringSlice, Desc: "标签列表"},
|
||||
{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码必须大于 0"},
|
||||
{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量必须在 1 到 100 之间"},
|
||||
},
|
||||
[]contract.ParamDecl{
|
||||
{Name: "keyword", Property: "keyword"}, {Name: "pool-type", Property: "poolType"},
|
||||
{Name: "creator", Property: "creator"}, {Name: "labels", Property: "labels"},
|
||||
{Name: "page", Property: "currentPage", InterfaceType: "number"}, {Name: "page-size", Property: "pageSize", InterfaceType: "number"},
|
||||
},
|
||||
`dws hrbrain +list-pools --page 1 --page-size 20 --format json`,
|
||||
)
|
||||
declaration.Constraints = hrbrainPageConstraints()
|
||||
declaration.Validate = hrbrainValidatePage
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
params := map[string]any{"currentPage": rt.Int("page"), "pageSize": rt.Int("page-size")}
|
||||
hrbrainAddOptional(params, rt, "keyword", "keyword")
|
||||
hrbrainAddOptional(params, rt, "pool-type", "poolType")
|
||||
hrbrainAddOptional(params, rt, "creator", "creator")
|
||||
if rt.Changed("labels") {
|
||||
params["labels"] = hrbrainCleanValues(rt.StrSlice("labels"))
|
||||
}
|
||||
return hrbrainCallPage(rt, "list_talent_pools", params, "poolCode")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var GetPool = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+get-pool", "根据 poolCode 查询人才池详情", "已知稳定 poolCode 并需要读取同一人才池详情时使用;没有安全非空 fixture 前保持 unavailable。",
|
||||
hrbrainObjectResult("身份匹配的人才池详情"), nil,
|
||||
[]shortcut.Flag{{Name: "pool-code", Type: shortcut.FlagString, Required: true, Desc: "人才池稳定 poolCode"}},
|
||||
[]contract.ParamDecl{{Name: "pool-code", Property: "poolCode"}},
|
||||
`dws hrbrain +get-pool --pool-code <POOL_CODE> --format json`,
|
||||
)
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
return hrbrainCallObject(rt, "get_talent_pool_detail", map[string]any{"poolCode": rt.Str("pool-code")}, rt.Str("pool-code"), "poolCode")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var ListPoolEmployees = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+list-pool-employees", "查询指定人才池内的员工列表", "已知稳定 poolCode 并需要分页读取池内员工时使用;缺少安全人才池与员工 fixture 前保持 unavailable。",
|
||||
hrbrainPageResult("严格校验的人才池员工页"), hrbrainPagination(),
|
||||
[]shortcut.Flag{
|
||||
{Name: "pool-code", Type: shortcut.FlagString, Required: true, Desc: "人才池稳定 poolCode"},
|
||||
{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码必须大于 0"},
|
||||
{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量必须在 1 到 100 之间"},
|
||||
},
|
||||
[]contract.ParamDecl{
|
||||
{Name: "pool-code", Property: "poolCode"}, {Name: "page", Property: "currentPage", InterfaceType: "number"},
|
||||
{Name: "page-size", Property: "pageSize", InterfaceType: "number"},
|
||||
},
|
||||
`dws hrbrain +list-pool-employees --pool-code <POOL_CODE> --page 1 --page-size 20 --format json`,
|
||||
)
|
||||
declaration.Constraints = hrbrainPageConstraints()
|
||||
declaration.Validate = hrbrainValidatePage
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
return hrbrainCallPage(rt, "list_pool_employees", map[string]any{
|
||||
"poolCode": rt.Str("pool-code"), "currentPage": rt.Int("page"), "pageSize": rt.Int("page-size"),
|
||||
}, "workNo", "staffId", "userId")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var ProfileMetadata = hrbrainCollectionShortcut(
|
||||
"+profile-metadata", "查询员工档案元数据结构", "已知工号并需要发现可查询档案模块与字段时使用;下游工具当前不可验证,保持 unavailable。",
|
||||
"get_profile_metadata", "work-no", "workNo", "档案元数据", []string{"modelCode", "fieldCode", "code"},
|
||||
)
|
||||
|
||||
var QueryProfile = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+query-profile", "按模块批量查询员工档案数据", "已知工号、模块与字段编码并需要批量读取档案时使用;下游可用性和结果合同未通过 live 验证。",
|
||||
hrbrainCollectionResult("严格校验的员工档案模块结果"), nil,
|
||||
[]shortcut.Flag{
|
||||
{Name: "work-no", Type: shortcut.FlagString, Required: true, Desc: "员工稳定工号"},
|
||||
{Name: "data-queries", Type: shortcut.FlagString, Required: true, Desc: "必须是非空 JSON 数组的模块查询条件"},
|
||||
},
|
||||
[]contract.ParamDecl{{Name: "work-no", Property: "workNo"}, {Name: "data-queries", Property: "dataQueries"}},
|
||||
`dws hrbrain +query-profile --work-no <WORK_NO> --data-queries '[{"modelCode":"basic","fields":["name"]}]' --format json`,
|
||||
)
|
||||
declaration.Constraints = []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"data-queries"}, Description: "必须是非空 JSON 数组的模块查询条件"}}
|
||||
declaration.Validate = func(rt *shortcut.RuntimeContext) error {
|
||||
_, err := hrbrainJSONArray(rt.Str("data-queries"), "--data-queries")
|
||||
return err
|
||||
}
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
// Validate already established the exact non-empty object-array shape.
|
||||
queries, _ := hrbrainJSONArray(rt.Str("data-queries"), "--data-queries")
|
||||
return hrbrainCallCollection(rt, "query_profile_data", map[string]any{"workNo": rt.Str("work-no"), "dataQueries": queries}, "modelCode", "moduleCode")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var ProfileLabels = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+profile-labels", "批量查询员工档案标签", "已知一组稳定工号并需要读取标签时使用;下游当前 success=true 但 result=null,保持 unavailable。",
|
||||
hrbrainCollectionResult("严格校验的员工标签结果"), nil,
|
||||
[]shortcut.Flag{
|
||||
{Name: "staff-ids", Type: shortcut.FlagStringSlice, Required: true, Desc: "一个或多个非空员工工号"},
|
||||
{Name: "all-label", Type: shortcut.FlagBool, Desc: "是否返回全部标签"},
|
||||
},
|
||||
[]contract.ParamDecl{{Name: "staff-ids", Property: "staffIds"}, {Name: "all-label", Property: "allLabel"}},
|
||||
`dws hrbrain +profile-labels --staff-ids <WORK_NO> --format json`,
|
||||
)
|
||||
declaration.Constraints = []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"staff-ids"}, Description: "必须包含一个或多个非空员工工号"}}
|
||||
declaration.Validate = func(rt *shortcut.RuntimeContext) error {
|
||||
if len(hrbrainCleanValues(rt.StrSlice("staff-ids"))) == 0 {
|
||||
return apperrors.NewValidation("--staff-ids 必须包含一个或多个非空员工工号")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
params := map[string]any{"staffIds": hrbrainCleanValues(rt.StrSlice("staff-ids"))}
|
||||
if rt.Changed("all-label") {
|
||||
params["allLabel"] = rt.Bool("all-label")
|
||||
}
|
||||
return hrbrainCallCollection(rt, "get_profile_label", params, "workNo", "staffId", "userId")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var ProfileCareer = hrbrainCollectionShortcut(
|
||||
"+profile-career", "查询员工公司内职业历程", "已知稳定工号并需要读取岗位或职级变动历史时使用;下游未通过 live 可用性验证。",
|
||||
"get_employee_career", "work-no", "workNo", "职业历程", []string{"careerId", "id", "workNo"},
|
||||
)
|
||||
|
||||
var ProfilePerformance = hrbrainCollectionShortcut(
|
||||
"+profile-performance", "查询员工历史绩效记录", "已知稳定工号并需要读取历史绩效时使用;下游未通过 live 可用性验证。",
|
||||
"get_employee_performance", "work-no", "workNo", "绩效记录", []string{"performanceId", "id", "workNo"},
|
||||
)
|
||||
|
||||
var SearchEmployees = func() shortcut.Shortcut {
|
||||
declaration := hrbrainEmployeeSearchBase(
|
||||
"+search-employees", "按关键词、部门、职务、职级或人才池搜索员工", "需要按至少一个基础过滤条件搜索员工时使用;当前下游返回业务错误且缺少安全非空 fixture。",
|
||||
"search_employees",
|
||||
)
|
||||
declaration.Constraints = append(hrbrainPageConstraints(), shortcut.Constraint{
|
||||
Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"keyword", "dept-name", "position-name", "job-level", "pool-code"},
|
||||
Description: "至少提供一个员工搜索过滤条件,禁止无条件枚举组织人员",
|
||||
})
|
||||
declaration.Validate = hrbrainValidatePage
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
params := map[string]any{"currentPage": rt.Int("page"), "pageSize": rt.Int("page-size")}
|
||||
for _, pair := range [][2]string{{"keyword", "keyword"}, {"dept-name", "deptName"}, {"position-name", "positionName"}, {"job-level", "jobLevel"}, {"pool-code", "poolCode"}} {
|
||||
hrbrainAddOptional(params, rt, pair[0], pair[1])
|
||||
}
|
||||
return hrbrainCallPage(rt, "search_employees", params, "workNo", "staffId", "userId")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var SearchEmployeesStructured = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+search-employees-structured", "使用高级条件表达式搜索员工", "已先取得有权限字段,并需要组合过滤表达式搜索员工时使用;下游工具当前不可用。",
|
||||
hrbrainPageResult("严格校验的高级员工搜索页"), hrbrainPagination(),
|
||||
[]shortcut.Flag{
|
||||
{Name: "origin-json", Type: shortcut.FlagString, Required: true, Desc: "必须是非空 JSON 对象的搜索表达式"},
|
||||
{Name: "fields", Type: shortcut.FlagString, Required: true, Desc: "必须是非空 JSON 数组的返回字段"},
|
||||
{Name: "order-by", Type: shortcut.FlagStringSlice, Desc: "排序字段列表"},
|
||||
{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码必须大于 0"},
|
||||
{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量必须在 1 到 100 之间"},
|
||||
},
|
||||
[]contract.ParamDecl{
|
||||
{Name: "origin-json", Property: "originJson"}, {Name: "fields", Property: "fields"}, {Name: "order-by", Property: "orderByClauses"},
|
||||
{Name: "page", Property: "currentPage", InterfaceType: "number"}, {Name: "page-size", Property: "pageSize", InterfaceType: "number"},
|
||||
},
|
||||
`dws hrbrain +search-employees-structured --origin-json '{"rules":[],"combinator":"and"}' --fields '[{"label":"name","value":"name"}]' --format json`,
|
||||
)
|
||||
declaration.Constraints = append(hrbrainPageConstraints(),
|
||||
shortcut.Constraint{Kind: shortcut.ConstraintCustom, Flags: []string{"origin-json"}, Description: "必须是非空 JSON 对象的搜索表达式"},
|
||||
shortcut.Constraint{Kind: shortcut.ConstraintCustom, Flags: []string{"fields"}, Description: "必须是非空 JSON 数组的返回字段"},
|
||||
)
|
||||
declaration.Validate = func(rt *shortcut.RuntimeContext) error {
|
||||
if err := hrbrainValidatePage(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := hrbrainJSONObject(rt.Str("origin-json"), "--origin-json"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := hrbrainJSONArray(rt.Str("fields"), "--fields")
|
||||
return err
|
||||
}
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
// Validate already established both structured input shapes.
|
||||
origin, _ := hrbrainJSONObject(rt.Str("origin-json"), "--origin-json")
|
||||
fields, _ := hrbrainJSONArray(rt.Str("fields"), "--fields")
|
||||
params := map[string]any{
|
||||
"originJson": string(mustJSON(origin)), "fields": fields,
|
||||
"currentPage": rt.Int("page"), "pageSize": rt.Int("page-size"),
|
||||
}
|
||||
if rt.Changed("order-by") {
|
||||
params["orderByClauses"] = hrbrainCleanValues(rt.StrSlice("order-by"))
|
||||
}
|
||||
return hrbrainCallPage(rt, "search_employees_structured", params, "workNo", "staffId", "userId")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
var SearchFields = func() shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
"+search-fields", "获取当前身份可用的高级人才搜索字段", "构造高级员工搜索表达式前发现字段与操作符时使用;下游工具当前返回业务错误,保持 unavailable。",
|
||||
hrbrainCollectionResult("严格校验的高级搜索字段"), nil, nil, nil,
|
||||
`dws hrbrain +search-fields --format json`,
|
||||
)
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
return hrbrainCallCollection(rt, "get_search_fields", nil, "fieldCode", "code", "value")
|
||||
}
|
||||
return declaration
|
||||
}()
|
||||
|
||||
func hrbrainCollectionShortcut(command, description, intent, tool, flag, property, resultDescription string, identityKeys []string) shortcut.Shortcut {
|
||||
declaration := hrbrainBase(
|
||||
command, description, intent, hrbrainCollectionResult("严格校验的"+resultDescription), nil,
|
||||
[]shortcut.Flag{{Name: flag, Type: shortcut.FlagString, Required: true, Desc: "员工稳定工号"}},
|
||||
[]contract.ParamDecl{{Name: flag, Property: property}},
|
||||
fmt.Sprintf("dws hrbrain %s --%s <WORK_NO> --format json", command, flag),
|
||||
)
|
||||
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
|
||||
return hrbrainCallCollection(rt, tool, map[string]any{property: rt.Str(flag)}, identityKeys...)
|
||||
}
|
||||
return declaration
|
||||
}
|
||||
|
||||
func hrbrainEmployeeSearchBase(command, description, intent, _ string) shortcut.Shortcut {
|
||||
return hrbrainBase(
|
||||
command, description, intent, hrbrainPageResult("严格校验的员工搜索页"), hrbrainPagination(),
|
||||
[]shortcut.Flag{
|
||||
{Name: "keyword", Type: shortcut.FlagString, Desc: "姓名或工号关键词"},
|
||||
{Name: "dept-name", Type: shortcut.FlagString, Desc: "部门名称"},
|
||||
{Name: "position-name", Type: shortcut.FlagString, Desc: "职务名称"},
|
||||
{Name: "job-level", Type: shortcut.FlagString, Desc: "职级"},
|
||||
{Name: "pool-code", Type: shortcut.FlagString, Desc: "人才池稳定 poolCode"},
|
||||
{Name: "page", Type: shortcut.FlagInt, Default: "1", Desc: "页码必须大于 0"},
|
||||
{Name: "page-size", Type: shortcut.FlagInt, Default: "20", Desc: "每页数量必须在 1 到 100 之间"},
|
||||
},
|
||||
[]contract.ParamDecl{
|
||||
{Name: "keyword", Property: "keyword"}, {Name: "dept-name", Property: "deptName"}, {Name: "position-name", Property: "positionName"},
|
||||
{Name: "job-level", Property: "jobLevel"}, {Name: "pool-code", Property: "poolCode"},
|
||||
{Name: "page", Property: "currentPage", InterfaceType: "number"}, {Name: "page-size", Property: "pageSize", InterfaceType: "number"},
|
||||
},
|
||||
`dws hrbrain +search-employees --keyword <QUERY> --page 1 --page-size 20 --format json`,
|
||||
)
|
||||
}
|
||||
|
||||
func hrbrainPageConstraints() []shortcut.Constraint {
|
||||
return []shortcut.Constraint{
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"page"}, Description: "页码必须大于 0"},
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"page-size"}, Description: "每页数量必须在 1 到 100 之间"},
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainValidatePage(rt *shortcut.RuntimeContext) error {
|
||||
if rt.Int("page") < 1 {
|
||||
return apperrors.NewValidation("--page 必须大于 0")
|
||||
}
|
||||
if size := rt.Int("page-size"); size < 1 || size > 100 {
|
||||
return apperrors.NewValidation("--page-size 必须在 1 到 100 之间")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hrbrainCallObject(rt *shortcut.RuntimeContext, tool string, params map[string]any, expected string, identityKeys ...string) error {
|
||||
data, err := rt.CallMCPData("hrbrain", tool, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := responsecheck.RequireSingleObjectResult(data, "hrbrain/"+tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identity := hrbrainIdentity(value, identityKeys...)
|
||||
if identity == "" {
|
||||
return responsecheck.Error("hrbrain/"+tool, "missing_stable_id", "HRbrain 详情对象缺少稳定身份")
|
||||
}
|
||||
if expected != "" && identity != strings.TrimSpace(expected) {
|
||||
return responsecheck.Error("hrbrain/"+tool, "identity_mismatch", "HRbrain 详情对象身份与请求目标不一致")
|
||||
}
|
||||
return rt.Output(map[string]any{"value": value})
|
||||
}
|
||||
|
||||
func hrbrainCallCollection(rt *shortcut.RuntimeContext, tool string, params map[string]any, identityKeys ...string) error {
|
||||
data, err := rt.CallMCPData("hrbrain", tool, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := hrbrainRequireCollectionResult(data, "hrbrain/"+tool, identityKeys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rt.Output(map[string]any{"count": len(items), "items": items})
|
||||
}
|
||||
|
||||
func hrbrainCallPage(rt *shortcut.RuntimeContext, tool string, params map[string]any, identityKeys ...string) error {
|
||||
page, size := rt.Int("page"), rt.Int("page-size")
|
||||
data, err := rt.CallMCPData("hrbrain", tool, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, evidence, err := hrbrainProjectPage(data, "hrbrain/"+tool, page, size, identityKeys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"count": len(items), "items": items, "currentPage": evidence.CurrentPage,
|
||||
"pageSize": evidence.PageSize, "totalCount": evidence.TotalCount, "complete": !evidence.HasMore,
|
||||
}
|
||||
next := ""
|
||||
if evidence.HasMore {
|
||||
next = strconv.Itoa(evidence.CurrentPage + 1)
|
||||
}
|
||||
// The strict page evidence guarantees either terminal+empty-next or
|
||||
// continuing+positive next page, the two valid framework states.
|
||||
pagination, _ := output.NewPagination(!evidence.HasMore, next)
|
||||
return output.StoreResult(rt.Command().Context(), output.Success(payload, output.WithMeta(&output.Meta{
|
||||
Count: output.NewCount(len(items)), Pagination: pagination,
|
||||
})))
|
||||
}
|
||||
|
||||
func hrbrainRequireCollectionResult(data map[string]any, operation string, identityKeys ...string) ([]map[string]any, error) {
|
||||
result, err := responsecheck.RequireResult(data, operation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, ok := result.([]any)
|
||||
if !ok {
|
||||
return nil, responsecheck.Error(operation, "malformed_collection", fmt.Sprintf("响应 result 应为数组,实际为 %T", result))
|
||||
}
|
||||
return hrbrainValidateItems(raw, operation, identityKeys...)
|
||||
}
|
||||
|
||||
func hrbrainProjectPage(data map[string]any, operation string, requestedPage, requestedSize int, identityKeys ...string) ([]map[string]any, hrbrainPage, error) {
|
||||
result, err := responsecheck.RequireObjectResult(data, operation)
|
||||
if err != nil {
|
||||
return nil, hrbrainPage{}, err
|
||||
}
|
||||
raw, ok := result["items"].([]any)
|
||||
if !ok {
|
||||
if _, present := result["items"]; !present {
|
||||
return nil, hrbrainPage{}, responsecheck.Error(operation, "missing_collection", "成功响应缺少 result.items 数组")
|
||||
}
|
||||
return nil, hrbrainPage{}, responsecheck.Error(operation, "malformed_collection", fmt.Sprintf("响应 result.items 应为数组,实际为 %T", result["items"]))
|
||||
}
|
||||
items, err := hrbrainValidateItems(raw, operation, identityKeys...)
|
||||
if err != nil {
|
||||
return nil, hrbrainPage{}, err
|
||||
}
|
||||
evidence, err := hrbrainParsePage(result, operation, requestedPage, requestedSize, len(items))
|
||||
if err != nil {
|
||||
return nil, hrbrainPage{}, err
|
||||
}
|
||||
return items, evidence, nil
|
||||
}
|
||||
|
||||
func hrbrainValidateItems(raw []any, operation string, identityKeys ...string) ([]map[string]any, error) {
|
||||
items := make([]map[string]any, 0, len(raw))
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
for index, item := range raw {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok || len(object) == 0 {
|
||||
return nil, responsecheck.Error(operation, "malformed_item", fmt.Sprintf("结果第 %d 项不是非空对象", index))
|
||||
}
|
||||
identity := hrbrainIdentity(object, identityKeys...)
|
||||
if identity == "" {
|
||||
return nil, responsecheck.Error(operation, "missing_item_identity", fmt.Sprintf("结果第 %d 项缺少稳定身份", index))
|
||||
}
|
||||
if _, duplicate := seen[identity]; duplicate {
|
||||
return nil, responsecheck.Error(operation, "duplicate_item_identity", fmt.Sprintf("结果第 %d 项稳定身份重复", index))
|
||||
}
|
||||
seen[identity] = struct{}{}
|
||||
items = append(items, object)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func hrbrainParsePage(result map[string]any, operation string, requestedPage, requestedSize, itemCount int) (hrbrainPage, error) {
|
||||
currentPage, ok := hrbrainInteger(result["currentPage"])
|
||||
if !ok || currentPage != requestedPage || currentPage < 1 {
|
||||
return hrbrainPage{}, responsecheck.Error(operation, "pagination_page_mismatch", "currentPage 缺失、错型或与请求页码不一致")
|
||||
}
|
||||
pageSize, ok := hrbrainInteger(result["pageSize"])
|
||||
if !ok || pageSize != requestedSize || pageSize < 1 || itemCount > pageSize {
|
||||
return hrbrainPage{}, responsecheck.Error(operation, "pagination_size_mismatch", "pageSize 缺失、错型、与请求不一致或小于结果条数")
|
||||
}
|
||||
totalCount, ok := hrbrainInteger(result["totalCount"])
|
||||
if !ok || totalCount < 0 || totalCount < itemCount {
|
||||
return hrbrainPage{}, responsecheck.Error(operation, "invalid_total_count", "totalCount 缺失、错型、为负数或小于当前页条数")
|
||||
}
|
||||
hasMore, ok := result["hasMore"].(bool)
|
||||
if !ok {
|
||||
return hrbrainPage{}, responsecheck.Error(operation, "malformed_pagination", "hasMore 缺失或不是布尔值")
|
||||
}
|
||||
pageEnd := currentPage * pageSize
|
||||
if hasMore && pageEnd >= totalCount || !hasMore && pageEnd < totalCount {
|
||||
return hrbrainPage{}, responsecheck.Error(operation, "conflicting_pagination", "hasMore 与 currentPage/pageSize/totalCount 相互矛盾")
|
||||
}
|
||||
return hrbrainPage{CurrentPage: currentPage, PageSize: pageSize, TotalCount: totalCount, HasMore: hasMore}, nil
|
||||
}
|
||||
|
||||
func hrbrainIdentity(object map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
value := object[key]
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if text := strings.TrimSpace(typed); text != "" {
|
||||
return text
|
||||
}
|
||||
case float64:
|
||||
if typed == math.Trunc(typed) && !math.IsNaN(typed) && !math.IsInf(typed, 0) {
|
||||
return strconv.FormatInt(int64(typed), 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hrbrainInteger(value any) (int, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return typed, true
|
||||
case float64:
|
||||
if typed != math.Trunc(typed) || math.IsNaN(typed) || math.IsInf(typed, 0) || typed > float64(math.MaxInt) || typed < float64(math.MinInt) {
|
||||
return 0, false
|
||||
}
|
||||
return int(typed), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainAddOptional(params map[string]any, rt *shortcut.RuntimeContext, flag, property string) {
|
||||
if value := strings.TrimSpace(rt.Str(flag)); value != "" {
|
||||
params[property] = value
|
||||
}
|
||||
}
|
||||
|
||||
func hrbrainCleanValues(values []string) []string {
|
||||
cleaned := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
cleaned = append(cleaned, value)
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func hrbrainJSONArray(raw, flag string) ([]any, error) {
|
||||
var value []any
|
||||
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf("%s 必须是有效 JSON 数组: %v", flag, err))
|
||||
}
|
||||
if len(value) == 0 {
|
||||
return nil, apperrors.NewValidation(flag + " 必须是非空 JSON 数组")
|
||||
}
|
||||
for index, item := range value {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok || len(object) == 0 {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf("%s 第 %d 项必须是非空对象", flag, index))
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func hrbrainJSONObject(raw, flag string) (map[string]any, error) {
|
||||
var value map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf("%s 必须是有效 JSON 对象: %v", flag, err))
|
||||
}
|
||||
if len(value) == 0 {
|
||||
return nil, apperrors.NewValidation(flag + " 必须是非空 JSON 对象")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func mustJSON(value any) []byte {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
func init() {
|
||||
shortcut.Register(
|
||||
ListPools, GetPool, ListPoolEmployees,
|
||||
ProfileMetadata, QueryProfile, ProfileLabels, ProfileCareer, ProfilePerformance,
|
||||
SearchEmployees, SearchEmployeesStructured, SearchFields,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package hrbrain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type hrbrainCoverageCaller struct{ calls int }
|
||||
|
||||
func (caller *hrbrainCoverageCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
||||
caller.calls++
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{"success":true,"result":[]}`}}}, nil
|
||||
}
|
||||
func (*hrbrainCoverageCaller) Format() string { return "json" }
|
||||
func (*hrbrainCoverageCaller) DryRun() bool { return false }
|
||||
func (*hrbrainCoverageCaller) Fields() string { return "" }
|
||||
func (*hrbrainCoverageCaller) JQ() string { return "" }
|
||||
|
||||
type hrbrainParityCaller struct {
|
||||
calls int
|
||||
server string
|
||||
tool string
|
||||
params map[string]any
|
||||
response string
|
||||
err error
|
||||
}
|
||||
|
||||
func (caller *hrbrainParityCaller) CallTool(_ context.Context, server, tool string, params map[string]any) (*edition.ToolResult, error) {
|
||||
caller.calls++
|
||||
caller.server = server
|
||||
caller.tool = tool
|
||||
caller.params = params
|
||||
if caller.err != nil {
|
||||
return nil, caller.err
|
||||
}
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: caller.response}}}, nil
|
||||
}
|
||||
func (*hrbrainParityCaller) Format() string { return "json" }
|
||||
func (*hrbrainParityCaller) DryRun() bool { return false }
|
||||
func (*hrbrainParityCaller) Fields() string { return "" }
|
||||
func (*hrbrainParityCaller) JQ() string { return "" }
|
||||
|
||||
func runHRbrainCoverage(t *testing.T, declaration shortcut.Shortcut, caller edition.ToolCaller, args ...string) error {
|
||||
t.Helper()
|
||||
helpers.InitDepsForTest(t, caller)
|
||||
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
||||
root.PersistentFlags().Bool("yes", false, "")
|
||||
root.PersistentFlags().Bool("dry-run", false, "")
|
||||
root.PersistentFlags().String("format", "json", "")
|
||||
service := &cobra.Command{Use: "hrbrain"}
|
||||
service.AddCommand(corecmd.New(shortcut.FromShortcut(declaration)))
|
||||
root.AddCommand(service)
|
||||
root.SetOut(io.Discard)
|
||||
root.SetErr(io.Discard)
|
||||
ctx, _ := output.WithResultStore(context.Background())
|
||||
root.SetContext(ctx)
|
||||
root.SetArgs(append([]string{"hrbrain", declaration.Command}, args...))
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainDeclarationsStayUnavailableAndTyped(t *testing.T) {
|
||||
declarations := []shortcut.Shortcut{
|
||||
ListPools, GetPool, ListPoolEmployees, ProfileMetadata, QueryProfile, ProfileLabels,
|
||||
ProfileCareer, ProfilePerformance, SearchEmployees, SearchEmployeesStructured, SearchFields,
|
||||
}
|
||||
if len(declarations) != 11 {
|
||||
t.Fatalf("declarations = %d", len(declarations))
|
||||
}
|
||||
for _, declaration := range declarations {
|
||||
if !declaration.Hidden || declaration.Availability != shortcut.AvailabilityUnavailable {
|
||||
t.Errorf("%s must remain hidden/unavailable", declaration.Command)
|
||||
}
|
||||
if declaration.OutputRollout != output.RolloutUnifiedActive || declaration.Contract.Empty() || declaration.Contract.Result == nil {
|
||||
t.Errorf("%s lacks unified Contract/Result", declaration.Command)
|
||||
}
|
||||
if strings.TrimSpace(declaration.Safety.Effect) == "" || declaration.Contract.Interface == nil || declaration.Contract.Interface.Availability != "unavailable" {
|
||||
t.Errorf("%s lacks unavailable interface and Safety", declaration.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainBlockersAreClassifiedWithoutShortcutDefects(t *testing.T) {
|
||||
commands := []string{
|
||||
ListPools.Command, GetPool.Command, ListPoolEmployees.Command, ProfileMetadata.Command,
|
||||
QueryProfile.Command, ProfileLabels.Command, ProfileCareer.Command, ProfilePerformance.Command,
|
||||
SearchEmployees.Command, SearchEmployeesStructured.Command, SearchFields.Command,
|
||||
}
|
||||
counts := map[string]int{}
|
||||
for _, command := range commands {
|
||||
reason := hrbrainUnavailableReason(command)
|
||||
if strings.Contains(reason, "shortcut_defect") || !strings.Contains(reason, "classified=") {
|
||||
t.Fatalf("%s has unreviewed blocker reason %q", command, reason)
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(reason, "classified="+hrbrainBlockerAdapterBusiness):
|
||||
counts[hrbrainBlockerAdapterBusiness]++
|
||||
case strings.Contains(reason, "classified="+hrbrainBlockerTenantFixture):
|
||||
counts[hrbrainBlockerTenantFixture]++
|
||||
default:
|
||||
t.Fatalf("%s has unknown blocker classification %q", command, reason)
|
||||
}
|
||||
}
|
||||
if counts[hrbrainBlockerAdapterBusiness] != 9 || counts[hrbrainBlockerTenantFixture] != 2 {
|
||||
t.Fatalf("blocker counts = %#v", counts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainExactParameterProjectionForAllElevenShortcuts(t *testing.T) {
|
||||
pageResponse := `{"success":true,"result":{"items":[{"poolCode":"pool"}],"currentPage":1,"pageSize":20,"totalCount":1,"hasMore":false}}`
|
||||
employeePageResponse := `{"success":true,"result":{"items":[{"workNo":"worker"}],"currentPage":1,"pageSize":20,"totalCount":1,"hasMore":false}}`
|
||||
queryItems := []any{map[string]any{"modelCode": "model", "fields": []any{"field"}}}
|
||||
structuredFields := []any{map[string]any{"label": "field", "value": "field"}}
|
||||
cases := []struct {
|
||||
name string
|
||||
declaration shortcut.Shortcut
|
||||
args []string
|
||||
tool string
|
||||
params map[string]any
|
||||
response string
|
||||
}{
|
||||
{name: "list pools", declaration: ListPools, tool: "list_talent_pools", params: map[string]any{"currentPage": 1, "pageSize": 20}, response: pageResponse},
|
||||
{name: "get pool", declaration: GetPool, args: []string{"--pool-code", "pool"}, tool: "get_talent_pool_detail", params: map[string]any{"poolCode": "pool"}, response: `{"success":true,"result":{"poolCode":"pool"}}`},
|
||||
{name: "list pool employees", declaration: ListPoolEmployees, args: []string{"--pool-code", "pool"}, tool: "list_pool_employees", params: map[string]any{"poolCode": "pool", "currentPage": 1, "pageSize": 20}, response: employeePageResponse},
|
||||
{name: "profile metadata", declaration: ProfileMetadata, args: []string{"--work-no", "worker"}, tool: "get_profile_metadata", params: map[string]any{"workNo": "worker"}, response: `{"success":true,"result":[{"modelCode":"model"}]}`},
|
||||
{name: "query profile", declaration: QueryProfile, args: []string{"--work-no", "worker", "--data-queries", `[{"modelCode":"model","fields":["field"]}]`}, tool: "query_profile_data", params: map[string]any{"workNo": "worker", "dataQueries": queryItems}, response: `{"success":true,"result":[{"modelCode":"model"}]}`},
|
||||
{name: "profile labels", declaration: ProfileLabels, args: []string{"--staff-ids", "worker"}, tool: "get_profile_label", params: map[string]any{"staffIds": []string{"worker"}}, response: `{"success":true,"result":[{"workNo":"worker"}]}`},
|
||||
{name: "profile career", declaration: ProfileCareer, args: []string{"--work-no", "worker"}, tool: "get_employee_career", params: map[string]any{"workNo": "worker"}, response: `{"success":true,"result":[{"careerId":"career"}]}`},
|
||||
{name: "profile performance", declaration: ProfilePerformance, args: []string{"--work-no", "worker"}, tool: "get_employee_performance", params: map[string]any{"workNo": "worker"}, response: `{"success":true,"result":[{"performanceId":"performance"}]}`},
|
||||
{name: "search employees", declaration: SearchEmployees, args: []string{"--keyword", "worker"}, tool: "search_employees", params: map[string]any{"keyword": "worker", "currentPage": 1, "pageSize": 20}, response: employeePageResponse},
|
||||
{name: "search employees structured", declaration: SearchEmployeesStructured, args: []string{"--origin-json", `{"rules":[{"field":"field"}]}`, "--fields", `[{"label":"field","value":"field"}]`}, tool: "search_employees_structured", params: map[string]any{"originJson": `{"rules":[{"field":"field"}]}`, "fields": structuredFields, "currentPage": 1, "pageSize": 20}, response: employeePageResponse},
|
||||
{name: "search fields", declaration: SearchFields, tool: "get_search_fields", params: map[string]any{}, response: `{"success":true,"result":[{"fieldCode":"field"}]}`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller := &hrbrainParityCaller{response: tc.response}
|
||||
if err := runHRbrainCoverage(t, tc.declaration, caller, tc.args...); err != nil {
|
||||
t.Fatalf("exact Shortcut failed against valid synthetic response: %v", err)
|
||||
}
|
||||
if caller.calls != 1 || caller.server != "hrbrain" || caller.tool != tc.tool {
|
||||
t.Fatalf("remote route = calls:%d server:%q tool:%q, want 1/hrbrain/%s", caller.calls, caller.server, caller.tool, tc.tool)
|
||||
}
|
||||
if !reflect.DeepEqual(caller.params, tc.params) {
|
||||
t.Fatalf("params = %#v, want %#v", caller.params, tc.params)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainRejectsNullWrongCollectionsAndBadItems(t *testing.T) {
|
||||
brokenCollections := []map[string]any{
|
||||
{},
|
||||
{"success": false, "result": []any{}},
|
||||
{"success": true, "result": nil},
|
||||
{"success": true, "result": map[string]any{}},
|
||||
{"success": true, "result": []any{"bad"}},
|
||||
{"success": true, "result": []any{map[string]any{"name": "missing-id"}}},
|
||||
{"success": true, "result": []any{map[string]any{"poolCode": "same"}, map[string]any{"poolCode": "same"}}},
|
||||
}
|
||||
for index, payload := range brokenCollections {
|
||||
if got, err := hrbrainRequireCollectionResult(payload, "hrbrain/test", "poolCode"); err == nil {
|
||||
t.Errorf("broken collection %d accepted: %#v", index, got)
|
||||
}
|
||||
}
|
||||
explicitEmpty := map[string]any{"success": true, "result": []any{}}
|
||||
if got, err := hrbrainRequireCollectionResult(explicitEmpty, "hrbrain/test", "poolCode"); err != nil || len(got) != 0 {
|
||||
t.Fatalf("explicit empty collection: got=%#v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainPaginationFailsClosed(t *testing.T) {
|
||||
valid := map[string]any{"success": true, "result": map[string]any{
|
||||
"items": []any{map[string]any{"poolCode": "pool-1"}},
|
||||
"currentPage": float64(1), "pageSize": float64(1), "totalCount": float64(2), "hasMore": true,
|
||||
}}
|
||||
items, page, err := hrbrainProjectPage(valid, "hrbrain/test", 1, 1, "poolCode")
|
||||
if err != nil || len(items) != 1 || !page.HasMore {
|
||||
t.Fatalf("valid page: items=%#v page=%+v err=%v", items, page, err)
|
||||
}
|
||||
broken := []map[string]any{
|
||||
{"success": true, "result": map[string]any{"items": []any{}}},
|
||||
{"success": true, "result": map[string]any{"items": "bad", "currentPage": float64(1), "pageSize": float64(20), "totalCount": float64(0), "hasMore": false}},
|
||||
{"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(2), "pageSize": float64(20), "totalCount": float64(0), "hasMore": false}},
|
||||
{"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(20), "totalCount": float64(21), "hasMore": false}},
|
||||
{"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(20), "totalCount": float64(20), "hasMore": true}},
|
||||
{"success": true, "result": map[string]any{"items": []any{}, "currentPage": float64(1), "pageSize": float64(20), "totalCount": float64(0), "hasMore": "false"}},
|
||||
}
|
||||
for index, payload := range broken {
|
||||
if got, evidence, err := hrbrainProjectPage(payload, "hrbrain/test", 1, 20, "poolCode"); err == nil {
|
||||
t.Errorf("broken page %d accepted: items=%#v evidence=%+v", index, got, evidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainStructuredInputsRejectBadShapes(t *testing.T) {
|
||||
for _, raw := range []string{"", "{}", "[]", "[1]", "[{}]"} {
|
||||
if _, err := hrbrainJSONArray(raw, "--items"); err == nil {
|
||||
t.Errorf("bad array accepted: %q", raw)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{"", "[]", "{}"} {
|
||||
if _, err := hrbrainJSONObject(raw, "--object"); err == nil {
|
||||
t.Errorf("bad object accepted: %q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainInvalidInputMakesZeroRemoteCalls(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
declaration shortcut.Shortcut
|
||||
args []string
|
||||
}{
|
||||
"unfiltered employee search": {declaration: SearchEmployees},
|
||||
"invalid page": {declaration: ListPools, args: []string{"--page", "0"}},
|
||||
"invalid page size": {declaration: ListPoolEmployees, args: []string{"--pool-code", "fixture", "--page-size", "101"}},
|
||||
"bad query json": {declaration: QueryProfile, args: []string{"--work-no", "fixture", "--data-queries", `{}`}},
|
||||
"bad structured json": {declaration: SearchEmployeesStructured, args: []string{"--origin-json", `{}`, "--fields", `[]`}},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
caller := &hrbrainCoverageCaller{}
|
||||
if err := runHRbrainCoverage(t, tc.declaration, caller, tc.args...); err == nil {
|
||||
t.Fatal("invalid input unexpectedly succeeded")
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("remote calls before validation = %d, want 0", caller.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainExecutionFailuresAndContinuation(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
declaration shortcut.Shortcut
|
||||
args []string
|
||||
response string
|
||||
err error
|
||||
}{
|
||||
"object transport": {declaration: GetPool, args: []string{"--pool-code", "pool"}, err: errors.New("transport")},
|
||||
"object malformed": {declaration: GetPool, args: []string{"--pool-code", "pool"}, response: `{"success":true,"result":null}`},
|
||||
"object missing id": {declaration: GetPool, args: []string{"--pool-code", "pool"}, response: `{"success":true,"result":{"name":"pool"}}`},
|
||||
"object wrong id": {declaration: GetPool, args: []string{"--pool-code", "pool"}, response: `{"success":true,"result":{"poolCode":"other"}}`},
|
||||
"collection transport": {declaration: ProfileLabels, args: []string{"--staff-ids", "worker"}, err: errors.New("transport")},
|
||||
"collection malformed": {declaration: ProfileLabels, args: []string{"--staff-ids", "worker"}, response: `{"success":true,"result":null}`},
|
||||
"page transport": {declaration: SearchEmployees, args: []string{"--keyword", "worker"}, err: errors.New("transport")},
|
||||
"page malformed": {declaration: SearchEmployees, args: []string{"--keyword", "worker"}, response: `{"success":true,"result":null}`},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
caller := &hrbrainParityCaller{response: tc.response, err: tc.err}
|
||||
if err := runHRbrainCoverage(t, tc.declaration, caller, tc.args...); err == nil {
|
||||
t.Fatal("invalid remote result succeeded")
|
||||
}
|
||||
if caller.calls != 1 {
|
||||
t.Fatalf("calls = %d", caller.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
continuing := &hrbrainParityCaller{response: `{"success":true,"result":{"items":[{"workNo":"worker"}],"currentPage":1,"pageSize":1,"totalCount":2,"hasMore":true}}`}
|
||||
if err := runHRbrainCoverage(t, SearchEmployees, continuing, "--keyword", "worker", "--page-size", "1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if continuing.calls != 1 {
|
||||
t.Fatalf("continuing calls = %d", continuing.calls)
|
||||
}
|
||||
|
||||
optionalPool := &hrbrainParityCaller{response: `{"success":true,"result":{"items":[{"poolCode":"pool"}],"currentPage":1,"pageSize":20,"totalCount":1,"hasMore":false}}`}
|
||||
if err := runHRbrainCoverage(t, ListPools, optionalPool,
|
||||
"--keyword", "pool", "--pool-type", "type", "--creator", "creator", "--labels", "one,two"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if labels, ok := optionalPool.params["labels"].([]string); !ok || len(labels) != 2 {
|
||||
t.Fatalf("labels = %#v", optionalPool.params["labels"])
|
||||
}
|
||||
|
||||
labelsCaller := &hrbrainParityCaller{response: `{"success":true,"result":[{"workNo":"worker"}]}`}
|
||||
if err := runHRbrainCoverage(t, ProfileLabels, labelsCaller, "--staff-ids", "worker", "--all-label"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if labelsCaller.params["allLabel"] != true {
|
||||
t.Fatalf("allLabel = %#v", labelsCaller.params["allLabel"])
|
||||
}
|
||||
|
||||
structured := &hrbrainParityCaller{response: `{"success":true,"result":{"items":[{"workNo":"worker"}],"currentPage":1,"pageSize":20,"totalCount":1,"hasMore":false}}`}
|
||||
if err := runHRbrainCoverage(t, SearchEmployeesStructured, structured,
|
||||
"--origin-json", `{"rules":[{"field":"field"}]}`, "--fields", `[{"label":"field","value":"field"}]`, "--order-by", "field"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if order, ok := structured.params["orderByClauses"].([]string); !ok || len(order) != 1 {
|
||||
t.Fatalf("orderByClauses = %#v", structured.params["orderByClauses"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainRemainingValidationAndScalarEdges(t *testing.T) {
|
||||
t.Run("unknown blocker", func(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("unknown blocker classification did not panic")
|
||||
}
|
||||
}()
|
||||
_ = hrbrainUnavailableReason("+unknown")
|
||||
})
|
||||
|
||||
profileLabelsCommand := corecmd.New(shortcut.FromShortcut(ProfileLabels))
|
||||
if err := profileLabelsCommand.Flags().Set("staff-ids", ","); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ProfileLabels.Validate(shortcut.RuntimeContextForTest(profileLabelsCommand, ProfileLabels)); err == nil {
|
||||
t.Fatal("blank staff IDs succeeded")
|
||||
}
|
||||
badPage := &hrbrainCoverageCaller{}
|
||||
if err := runHRbrainCoverage(t, SearchEmployeesStructured, badPage,
|
||||
"--origin-json", `{"rule":"value"}`, "--fields", `[{"field":"value"}]`, "--page", "0"); err == nil {
|
||||
t.Fatal("structured search invalid page succeeded")
|
||||
}
|
||||
if badPage.calls != 0 {
|
||||
t.Fatalf("structured invalid page remote calls = %d", badPage.calls)
|
||||
}
|
||||
|
||||
if got := hrbrainIdentity(map[string]any{"id": float64(7)}, "id"); got != "7" {
|
||||
t.Fatalf("numeric identity = %q", got)
|
||||
}
|
||||
for _, value := range []any{math.NaN(), math.Inf(1), 1.5} {
|
||||
if got := hrbrainIdentity(map[string]any{"id": value}, "id"); got != "" {
|
||||
t.Fatalf("invalid identity = %q", got)
|
||||
}
|
||||
}
|
||||
if got, ok := hrbrainInteger(7); !ok || got != 7 {
|
||||
t.Fatalf("integer = %d, %v", got, ok)
|
||||
}
|
||||
for _, value := range []any{math.NaN(), math.Inf(1), 1.5, float64(math.MaxInt) * 2, "7"} {
|
||||
if _, ok := hrbrainInteger(value); ok {
|
||||
t.Fatalf("invalid integer accepted: %#v", value)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("marshal panic", func(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("mustJSON did not panic for unsupported value")
|
||||
}
|
||||
}()
|
||||
_ = mustJSON(make(chan int))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageHRbrainPageSizeAndTotalContradictions(t *testing.T) {
|
||||
missing := map[string]any{"success": true, "result": map[string]any{}}
|
||||
if _, _, err := hrbrainProjectPage(missing, "hrbrain/test", 1, 1, "workNo"); err == nil {
|
||||
t.Fatal("missing page collection succeeded")
|
||||
}
|
||||
badItem := map[string]any{"success": true, "result": map[string]any{
|
||||
"items": []any{"bad"}, "currentPage": float64(1), "pageSize": float64(1), "totalCount": float64(1), "hasMore": false,
|
||||
}}
|
||||
if _, _, err := hrbrainProjectPage(badItem, "hrbrain/test", 1, 1, "workNo"); err == nil {
|
||||
t.Fatal("bad page item succeeded")
|
||||
}
|
||||
tooMany := map[string]any{"success": true, "result": map[string]any{
|
||||
"items": []any{map[string]any{"workNo": "one"}, map[string]any{"workNo": "two"}},
|
||||
"currentPage": float64(1), "pageSize": float64(1), "totalCount": float64(2), "hasMore": true,
|
||||
}}
|
||||
if _, _, err := hrbrainProjectPage(tooMany, "hrbrain/test", 1, 1, "workNo"); err == nil {
|
||||
t.Fatal("page larger than pageSize succeeded")
|
||||
}
|
||||
badTotal := map[string]any{"success": true, "result": map[string]any{
|
||||
"items": []any{map[string]any{"workNo": "one"}},
|
||||
"currentPage": float64(1), "pageSize": float64(1), "totalCount": float64(0), "hasMore": false,
|
||||
}}
|
||||
if _, _, err := hrbrainProjectPage(badTotal, "hrbrain/test", 1, 1, "workNo"); err == nil {
|
||||
t.Fatal("total below item count succeeded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
// Package pat registers safe Shortcut routing for PAT authorization tasks.
|
||||
package pat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
patcore "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pat"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
var safeAgentCodePattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
|
||||
|
||||
var (
|
||||
patBrowserPolicyConfigDir = patcore.BrowserPolicyConfigDir
|
||||
patSetBrowserPolicy = patcore.SetBrowserPolicy
|
||||
patReadBrowserPolicy = patcore.ReadStoredBrowserPolicy
|
||||
)
|
||||
|
||||
func browserPolicyResult() *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{"type":"object","description":"本地 PAT 浏览器策略预览或已验证写入结果","properties":{"scope":{"type":"string","description":"写入范围:default 或 agent","enum":["default","agent"]},"openBrowser":{"type":"boolean","description":"策略是否允许打开浏览器"},"executed":{"type":"boolean","description":"本次是否实际写入本地策略"},"verified":{"type":"boolean","description":"是否对同一目标完成磁盘读回验证"}},"required":["scope","openBrowser","executed","verified"],"additionalProperties":false}`),
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeResult() *contract.ResultSpec {
|
||||
return &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePending, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{"type":"object","description":"由 routed 原子命令 pat chmod 持有的 PAT 授权计划或逐 scope 终态账本","properties":{"selected":{"type":"integer","description":"计划选择的 scope 数量"},"skipped":{"type":"integer","description":"计划跳过的 scope 数量"},"pending":{"type":"integer","description":"仍待授权的 scope 数量"},"verified":{"type":"boolean","description":"授权身份与逐 scope 终态是否已验证"}},"required":["selected","skipped","pending","verified"],"additionalProperties":false}`),
|
||||
}
|
||||
}
|
||||
|
||||
var BrowserPolicy = shortcut.Shortcut{
|
||||
OutputRollout: output.RolloutUnifiedActive,
|
||||
Service: "pat",
|
||||
Command: "+browser-policy",
|
||||
Product: "pat",
|
||||
Description: "安全配置 PAT 授权时是否允许打开本地浏览器",
|
||||
Intent: "需要为默认策略或一个明确 Agent 配置 PAT 撞墙时是否允许打开本地浏览器时使用;支持无写入预览,实际写入需确认并对同一策略项完成磁盘读回。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "idempotent",
|
||||
},
|
||||
Contract: corecmd.ContractDecl{
|
||||
Identity: contract.ToolIdentitySpec{
|
||||
ProductID: "pat", Name: "shortcut_browser_policy", CanonicalPath: "pat.shortcut_browser_policy",
|
||||
CLIPath: "pat +browser-policy", PrimaryCLIPath: "pat +browser-policy",
|
||||
},
|
||||
Description: "安全配置 PAT 授权时是否允许打开本地浏览器",
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable,
|
||||
Reason: "The Shortcut writes only the local PAT policy file, requires confirmation, supports a no-write request preview, omits agent identity from output, and verifies the exact persisted target.",
|
||||
},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "安全配置 PAT 授权时是否允许打开本地浏览器",
|
||||
UseWhen: []string{"需要为默认策略或一个明确 Agent 配置 PAT 撞墙时是否允许打开本地浏览器时使用;支持无写入预览,实际写入需确认并对同一策略项完成磁盘读回。"},
|
||||
AvoidWhen: []string{"需要预览或授予行为 scope 时使用原子命令 pat chmod;普通 OAuth 登录使用 auth"},
|
||||
Examples: []string{`dws pat +browser-policy --enabled=false --dry-run --format json`},
|
||||
},
|
||||
Parameters: []contract.ParamDecl{
|
||||
{Name: "enabled"}, {Name: "agent-code"},
|
||||
},
|
||||
DryRun: &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewRequest},
|
||||
Result: browserPolicyResult(),
|
||||
},
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "enabled", Type: shortcut.FlagBool, Required: true, Desc: "是否允许打开本地浏览器;必须显式传 true 或 false"},
|
||||
{Name: "agent-code", Type: shortcut.FlagString, Desc: "可选 Agent 标识必须为 1 到 64 位字母、数字、下划线或连字符;只用于定位本地策略项且不会出现在结果中"},
|
||||
},
|
||||
Constraints: []shortcut.Constraint{
|
||||
{Kind: shortcut.ConstraintCustom, Flags: []string{"agent-code"}, Description: "可选 Agent 标识必须为 1 到 64 位字母、数字、下划线或连字符"},
|
||||
},
|
||||
Tips: []string{`dws pat +browser-policy --enabled=false --dry-run --format json`},
|
||||
Validate: func(rt *shortcut.RuntimeContext) error {
|
||||
value := strings.TrimSpace(rt.Str("agent-code"))
|
||||
if value != "" && !safeAgentCodePattern.MatchString(value) {
|
||||
return apperrors.NewValidation("--agent-code 格式无效;只允许 1 到 64 位字母、数字、下划线或连字符")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||||
scope := "default"
|
||||
agentCode := strings.TrimSpace(rt.Str("agent-code"))
|
||||
if agentCode != "" {
|
||||
scope = "agent"
|
||||
}
|
||||
enabled := rt.Bool("enabled")
|
||||
if rt.DryRun() {
|
||||
return rt.Output(map[string]any{"scope": scope, "openBrowser": enabled, "executed": false, "verified": false})
|
||||
}
|
||||
configDir := patBrowserPolicyConfigDir()
|
||||
written, err := patSetBrowserPolicy(configDir, agentCode, enabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stored, err := patReadBrowserPolicy(configDir, agentCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written.Scope != scope || stored.Scope != scope || written.OpenBrowser != enabled || stored.OpenBrowser != enabled || written.AgentCode != stored.AgentCode {
|
||||
return apperrors.NewInternal("PAT 浏览器策略写后读回不一致;结果未验证")
|
||||
}
|
||||
return rt.Output(map[string]any{"scope": scope, "openBrowser": enabled, "executed": true, "verified": true})
|
||||
},
|
||||
}
|
||||
|
||||
var Authorize = shortcut.Shortcut{
|
||||
OutputRollout: output.RolloutUnifiedActive,
|
||||
Service: "pat",
|
||||
Command: "+authorize",
|
||||
Product: "pat",
|
||||
Description: "将 PAT 行为 scope 授权路由到原子命令 pat chmod",
|
||||
Intent: "命令提示缺少 PAT 行为授权时 classified=routed:先使用 pat chmod --dry-run 审核计划,用户确认后再由同一原子命令执行;+authorize 本身保持 unavailable。",
|
||||
Risk: shortcut.RiskHighWrite,
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown",
|
||||
},
|
||||
Contract: corecmd.ContractDecl{
|
||||
Identity: contract.ToolIdentitySpec{
|
||||
ProductID: "pat", Name: "shortcut_authorize", CanonicalPath: "pat.shortcut_authorize",
|
||||
CLIPath: "pat +authorize", PrimaryCLIPath: "pat +authorize",
|
||||
},
|
||||
Description: "将 PAT 行为 scope 授权路由到原子命令 pat chmod",
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceUnavailable,
|
||||
Reason: "classified=routed; pat chmod already owns the reviewed plan, fallback, pending, per-scope ledger, identity, and confirmation chain. The Shortcut runtime cannot reuse that Cobra/ToolCaller closure without duplicating safety semantics, and a live write remains blocked_fixture, so +authorize stays unavailable while routing to pat chmod.",
|
||||
},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "将 PAT 行为 scope 授权路由到原子命令 pat chmod",
|
||||
UseWhen: []string{"命令提示缺少 PAT 行为授权时 classified=routed:先使用 pat chmod --dry-run 审核计划,用户确认后再由同一原子命令执行;+authorize 本身保持 unavailable。"},
|
||||
AvoidWhen: []string{"不要向此 unavailable Shortcut 传入任何真实身份、会话、scope 或授权材料;本地浏览器策略使用 pat +browser-policy"},
|
||||
Examples: []string{`dws pat +authorize --format json`},
|
||||
},
|
||||
Result: authorizeResult(),
|
||||
},
|
||||
Hidden: true,
|
||||
Availability: shortcut.AvailabilityUnavailable,
|
||||
Execute: func(*shortcut.RuntimeContext) error {
|
||||
return apperrors.NewValidation("PAT 授权任务 classified=routed;+authorize 当前 unavailable,请使用审核后的原子命令 pat chmod,并先执行 --dry-run")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
shortcut.Register(BrowserPolicy, Authorize)
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
package pat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
||||
patcore "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pat"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type patShortcutCaller struct{ calls int }
|
||||
|
||||
func (caller *patShortcutCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
||||
caller.calls++
|
||||
return nil, nil
|
||||
}
|
||||
func (*patShortcutCaller) Format() string { return "json" }
|
||||
func (*patShortcutCaller) DryRun() bool { return false }
|
||||
func (*patShortcutCaller) Fields() string { return "" }
|
||||
func (*patShortcutCaller) JQ() string { return "" }
|
||||
|
||||
type patAtomicCall struct {
|
||||
tool string
|
||||
args map[string]any
|
||||
}
|
||||
|
||||
type patAtomicFixtureCaller struct {
|
||||
dryRun bool
|
||||
responses []string
|
||||
calls []patAtomicCall
|
||||
}
|
||||
|
||||
func (caller *patAtomicFixtureCaller) CallTool(_ context.Context, _ string, tool string, args map[string]any) (*edition.ToolResult, error) {
|
||||
copied := make(map[string]any, len(args))
|
||||
for key, value := range args {
|
||||
copied[key] = value
|
||||
}
|
||||
caller.calls = append(caller.calls, patAtomicCall{tool: tool, args: copied})
|
||||
response := `{"success":true,"code":"OK","data":{}}`
|
||||
if index := len(caller.calls) - 1; index < len(caller.responses) {
|
||||
response = caller.responses[index]
|
||||
}
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: response}}}, nil
|
||||
}
|
||||
func (*patAtomicFixtureCaller) Format() string { return "json" }
|
||||
func (caller *patAtomicFixtureCaller) DryRun() bool { return caller.dryRun }
|
||||
func (*patAtomicFixtureCaller) Fields() string { return "" }
|
||||
func (*patAtomicFixtureCaller) JQ() string { return "" }
|
||||
|
||||
func runPATShortcut(t *testing.T, declaration shortcut.Shortcut, caller *patShortcutCaller, input string, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
helpers.InitDepsForTest(t, caller)
|
||||
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
||||
root.PersistentFlags().Bool("yes", false, "")
|
||||
root.PersistentFlags().Bool("dry-run", false, "")
|
||||
root.PersistentFlags().String("format", "json", "")
|
||||
ctx, _ := output.WithResultStore(context.Background())
|
||||
root.SetContext(ctx)
|
||||
service := &cobra.Command{Use: "pat"}
|
||||
service.AddCommand(corecmd.New(shortcut.FromShortcut(declaration)))
|
||||
root.AddCommand(service)
|
||||
var stdout bytes.Buffer
|
||||
root.SetOut(&stdout)
|
||||
root.SetErr(io.Discard)
|
||||
root.SetIn(strings.NewReader(input))
|
||||
root.SetArgs(append([]string{"pat", declaration.Command}, args...))
|
||||
executed, err := root.ExecuteC()
|
||||
if err == nil {
|
||||
_, _, err = output.EmitStoredResult(executed)
|
||||
}
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
func runPATAtomicRoute(t *testing.T, caller *patAtomicFixtureCaller, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
||||
root.PersistentFlags().Bool("yes", false, "")
|
||||
root.PersistentFlags().Bool("dry-run", false, "")
|
||||
root.PersistentFlags().String("format", "json", "")
|
||||
root.PersistentFlags().Bool("verbose", false, "")
|
||||
patcore.RegisterCommands(root, caller)
|
||||
root.SetOut(io.Discard)
|
||||
root.SetErr(io.Discard)
|
||||
root.SetIn(strings.NewReader(""))
|
||||
root.SetArgs(append([]string{"pat", "chmod"}, args...))
|
||||
|
||||
oldStdout := os.Stdout
|
||||
readPipe, writePipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = writePipe
|
||||
_, runErr := root.ExecuteC()
|
||||
_ = writePipe.Close()
|
||||
os.Stdout = oldStdout
|
||||
outputBytes, readErr := io.ReadAll(readPipe)
|
||||
_ = readPipe.Close()
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
return string(outputBytes), runErr
|
||||
}
|
||||
|
||||
func runPATNativeBrowserPolicy(t *testing.T, caller *patAtomicFixtureCaller, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
||||
root.PersistentFlags().Bool("yes", false, "")
|
||||
root.PersistentFlags().Bool("dry-run", false, "")
|
||||
root.PersistentFlags().String("format", "json", "")
|
||||
root.PersistentFlags().Bool("verbose", false, "")
|
||||
patcore.RegisterCommands(root, caller)
|
||||
var stdout bytes.Buffer
|
||||
root.SetOut(&stdout)
|
||||
root.SetErr(io.Discard)
|
||||
root.SetIn(strings.NewReader(""))
|
||||
root.SetArgs(append([]string{"pat", "browser-policy"}, args...))
|
||||
_, err := root.ExecuteC()
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATAuthorizeRouteUsesAtomicPlanFallbackPendingAndLedger(t *testing.T) {
|
||||
plan := `{"success":true,"code":"OK","data":{"items":[{"scope":"synthetic.item:read","disposition":"selected"}],"selectedScopes":["synthetic.item:read"],"skippedScopes":[],"pendingScopes":[]}}`
|
||||
pending := `{"success":true,"code":"OK","data":{"items":[{"scope":"synthetic.item:read","status":"pending"}],"selectedScopes":["synthetic.item:read"],"grantedScopes":[],"skippedScopes":[],"pendingScopes":["synthetic.item:read"]}}`
|
||||
|
||||
preconfirm := &patAtomicFixtureCaller{responses: []string{plan, pending}}
|
||||
if _, err := runPATAtomicRoute(t, preconfirm, "--product", "synthetic", "--grant-type", "once", "--format", "json"); err == nil {
|
||||
t.Fatal("unconfirmed atomic authorization unexpectedly succeeded")
|
||||
}
|
||||
if len(preconfirm.calls) != 0 {
|
||||
t.Fatalf("atomic calls before confirmation = %d, want 0", len(preconfirm.calls))
|
||||
}
|
||||
|
||||
dryRun := &patAtomicFixtureCaller{dryRun: true, responses: []string{plan}}
|
||||
dryOutput, err := runPATAtomicRoute(t, dryRun, "--product", "synthetic", "--grant-type", "once", "--dry-run", "--format", "json")
|
||||
if err != nil {
|
||||
t.Fatalf("atomic plan dry-run: %v", err)
|
||||
}
|
||||
if len(dryRun.calls) != 1 || dryRun.calls[0].tool != "pat.batch_plan" || dryRun.calls[0].args["dryRun"] != true {
|
||||
t.Fatalf("dry-run route = %#v", dryRun.calls)
|
||||
}
|
||||
if !strings.Contains(dryOutput, `"items"`) || !strings.Contains(dryOutput, `"selectedScopes"`) || !strings.Contains(dryOutput, `"pendingScopes"`) {
|
||||
t.Fatal("dry-run did not preserve the synthetic per-scope plan ledger")
|
||||
}
|
||||
|
||||
confirmed := &patAtomicFixtureCaller{responses: []string{plan, pending}}
|
||||
confirmedOutput, err := runPATAtomicRoute(t, confirmed, "--product", "synthetic", "--grant-type", "once", "--yes", "--format", "json")
|
||||
if err != nil {
|
||||
t.Fatalf("confirmed atomic route: %v", err)
|
||||
}
|
||||
if len(confirmed.calls) != 2 || confirmed.calls[0].tool != "pat.batch_plan" || confirmed.calls[1].tool != "pat.batch_grant" {
|
||||
t.Fatalf("confirmed route = %#v", confirmed.calls)
|
||||
}
|
||||
if scopes, ok := confirmed.calls[1].args["scopes"].([]string); !ok || len(scopes) != 1 || scopes[0] != "synthetic.item:read" {
|
||||
t.Fatalf("grant scopes = %#v", confirmed.calls[1].args["scopes"])
|
||||
}
|
||||
for _, key := range []string{"agentCode", "sessionId", "orgId", "uid"} {
|
||||
if _, present := confirmed.calls[1].args[key]; present {
|
||||
t.Fatalf("synthetic fixture unexpectedly carried identity key %s", key)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(confirmedOutput, `"items"`) || !strings.Contains(confirmedOutput, `"pendingScopes"`) || !strings.Contains(confirmedOutput, `"status":"pending"`) {
|
||||
t.Fatal("confirmed route did not preserve the synthetic pending per-scope ledger")
|
||||
}
|
||||
|
||||
fallback := &patAtomicFixtureCaller{responses: []string{
|
||||
`{"success":false,"errorCode":"PAT_BATCH_SCOPE_NOT_DECLARED","data":{}}`,
|
||||
`{"success":true,"code":"OK","data":{"items":[{"scope":"synthetic.item:read","status":"granted"}],"grantedScopes":["synthetic.item:read"],"pendingScopes":[]}}`,
|
||||
}}
|
||||
if _, err := runPATAtomicRoute(t, fallback, "synthetic.item:read", "--grant-type", "once", "--yes", "--format", "json"); err != nil {
|
||||
t.Fatalf("atomic fallback route: %v", err)
|
||||
}
|
||||
if len(fallback.calls) != 2 || fallback.calls[0].tool != "pat.batch_grant" || fallback.calls[1].tool != "pat.grant" {
|
||||
t.Fatalf("fallback route = %#v", fallback.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATBrowserPolicyConfirmationDryRunReadbackAndCleanup(t *testing.T) {
|
||||
proofRoot := t.TempDir()
|
||||
exactDir := filepath.Join(proofRoot, "exact")
|
||||
if err := os.Mkdir(exactDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("DWS_CONFIG_DIR", exactDir)
|
||||
policyPath := filepath.Join(exactDir, "pat_policy.json")
|
||||
caller := &patShortcutCaller{}
|
||||
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, caller, "", "--enabled=true"); err == nil {
|
||||
t.Fatal("unconfirmed write unexpectedly succeeded")
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("remote calls before confirmation = %d", caller.calls)
|
||||
}
|
||||
if _, err := os.Stat(policyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("policy file exists before confirmation: %v", err)
|
||||
}
|
||||
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, caller, "", "--enabled=true", "--dry-run"); err != nil {
|
||||
t.Fatalf("dry-run: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(policyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("dry-run created policy file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, caller, "yes\n", "--enabled=true"); err != nil {
|
||||
t.Fatalf("confirmed write: %v", err)
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("local policy Shortcut made %d remote calls", caller.calls)
|
||||
}
|
||||
stored, err := os.ReadFile(policyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read policy: %v", err)
|
||||
}
|
||||
if bytes.Contains(stored, []byte("agentCode")) || !bytes.Contains(stored, []byte(`"openBrowser": true`)) {
|
||||
t.Fatal("default policy did not persist the expected redacted setting")
|
||||
}
|
||||
if err := os.Remove(policyPath); err != nil {
|
||||
t.Fatalf("cleanup policy: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(policyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("policy cleanup left residue: %v", err)
|
||||
}
|
||||
|
||||
rawDir := filepath.Join(proofRoot, "raw")
|
||||
if err := os.Mkdir(rawDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("DWS_CONFIG_DIR", rawDir)
|
||||
rawPolicyPath := filepath.Join(rawDir, "pat_policy.json")
|
||||
rawCaller := &patAtomicFixtureCaller{}
|
||||
rawOutput, err := runPATNativeBrowserPolicy(t, rawCaller, "--enabled=false", "--format", "json")
|
||||
if err != nil {
|
||||
t.Fatalf("raw browser policy write: %v", err)
|
||||
}
|
||||
if len(rawCaller.calls) != 0 {
|
||||
t.Fatalf("raw local policy command made %d remote calls", len(rawCaller.calls))
|
||||
}
|
||||
var rawReceipt patcore.BrowserPolicySelection
|
||||
if err := json.Unmarshal([]byte(rawOutput), &rawReceipt); err != nil {
|
||||
t.Fatalf("parse raw browser policy receipt: %v", err)
|
||||
}
|
||||
if rawReceipt.Scope != "default" || rawReceipt.Source != "default" || rawReceipt.OpenBrowser || rawReceipt.AgentCode != "" {
|
||||
t.Fatal("raw browser policy did not return a terminal receipt for the requested target")
|
||||
}
|
||||
rawStored, err := os.ReadFile(rawPolicyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read raw policy: %v", err)
|
||||
}
|
||||
if bytes.Contains(rawStored, []byte("agentCode")) || !bytes.Contains(rawStored, []byte(`"openBrowser": false`)) {
|
||||
t.Fatal("raw default policy did not persist the expected redacted setting")
|
||||
}
|
||||
if err := os.Remove(rawPolicyPath); err != nil {
|
||||
t.Fatalf("cleanup raw policy: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(rawPolicyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("raw policy cleanup left residue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATBrowserPolicyOutputOmitsAgentIdentity(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
caller := &patShortcutCaller{}
|
||||
outputText, err := runPATShortcut(t, BrowserPolicy, caller, "", "--enabled=false", "--agent-code", "DWS_TEST_AGENT", "--yes")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(outputText, "DWS_TEST_AGENT") || strings.Contains(outputText, "agentCode") || strings.Contains(outputText, "agent-code") {
|
||||
t.Fatal("public PAT Shortcut output exposed agent identity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATBrowserPolicyWriteReadbackFailures(t *testing.T) {
|
||||
failure := errors.New("fixture failure")
|
||||
t.Run("write failure", func(t *testing.T) {
|
||||
testseam.Swap(t, &patBrowserPolicyConfigDir, func() string { return t.TempDir() })
|
||||
testseam.Swap(t, &patSetBrowserPolicy, func(string, string, bool) (patcore.BrowserPolicySelection, error) {
|
||||
return patcore.BrowserPolicySelection{}, failure
|
||||
})
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, &patShortcutCaller{}, "", "--enabled=true", "--yes"); !errors.Is(err, failure) {
|
||||
t.Fatalf("write error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read failure", func(t *testing.T) {
|
||||
testseam.Swap(t, &patBrowserPolicyConfigDir, func() string { return t.TempDir() })
|
||||
testseam.Swap(t, &patSetBrowserPolicy, func(string, string, bool) (patcore.BrowserPolicySelection, error) {
|
||||
return patcore.BrowserPolicySelection{Scope: "default", OpenBrowser: true}, nil
|
||||
})
|
||||
testseam.Swap(t, &patReadBrowserPolicy, func(string, string) (patcore.BrowserPolicySelection, error) {
|
||||
return patcore.BrowserPolicySelection{}, failure
|
||||
})
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, &patShortcutCaller{}, "", "--enabled=true", "--yes"); !errors.Is(err, failure) {
|
||||
t.Fatalf("read error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mismatched readback", func(t *testing.T) {
|
||||
testseam.Swap(t, &patBrowserPolicyConfigDir, func() string { return t.TempDir() })
|
||||
testseam.Swap(t, &patSetBrowserPolicy, func(string, string, bool) (patcore.BrowserPolicySelection, error) {
|
||||
return patcore.BrowserPolicySelection{Scope: "default", OpenBrowser: true}, nil
|
||||
})
|
||||
testseam.Swap(t, &patReadBrowserPolicy, func(string, string) (patcore.BrowserPolicySelection, error) {
|
||||
return patcore.BrowserPolicySelection{Scope: "default", OpenBrowser: false}, nil
|
||||
})
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, &patShortcutCaller{}, "", "--enabled=true", "--yes"); err == nil {
|
||||
t.Fatal("mismatched readback succeeded")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATInvalidAndUnavailablePathsMakeZeroRemoteCalls(t *testing.T) {
|
||||
caller := &patShortcutCaller{}
|
||||
if _, err := runPATShortcut(t, BrowserPolicy, caller, "", "--enabled=true", "--agent-code", "invalid value", "--yes"); err == nil {
|
||||
t.Fatal("invalid agent code unexpectedly succeeded")
|
||||
}
|
||||
if _, err := runPATShortcut(t, Authorize, caller, "", "--yes"); err == nil {
|
||||
t.Fatal("unavailable authorization Shortcut unexpectedly succeeded")
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("invalid/unavailable PAT paths made %d remote calls", caller.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoveragePATShortcutContracts(t *testing.T) {
|
||||
if BrowserPolicy.Hidden || BrowserPolicy.Availability == shortcut.AvailabilityUnavailable {
|
||||
t.Fatal("browser policy must be public/available before semantic decoration")
|
||||
}
|
||||
if BrowserPolicy.OutputRollout != output.RolloutUnifiedActive || BrowserPolicy.Contract.Result == nil || BrowserPolicy.Contract.DryRun == nil || BrowserPolicy.Contract.DryRun.PreviewKind != "request" || strings.TrimSpace(BrowserPolicy.Safety.Effect) == "" {
|
||||
t.Fatal("public browser policy lacks Result/Safety/DryRun/unified output")
|
||||
}
|
||||
if !Authorize.Hidden || Authorize.Availability != shortcut.AvailabilityUnavailable || Authorize.Contract.Interface.Availability != "unavailable" {
|
||||
t.Fatal("PAT authorization Shortcut must stay hidden/unavailable")
|
||||
}
|
||||
if !strings.Contains(Authorize.Contract.Interface.Reason, "classified=routed") || !strings.Contains(Authorize.Contract.Interface.Reason, "blocked_fixture") {
|
||||
t.Fatal("PAT authorization must separate the routed implementation from unproved live write evidence")
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,13 @@ func generatedPublicShortcutCatalog() map[string]struct{} {
|
||||
"aitable\u0000+dashboard-share-get": {},
|
||||
"aitable\u0000+dashboard-share-update": {},
|
||||
"aitable\u0000+dashboard-update": {},
|
||||
"aitable\u0000+datasource-create": {},
|
||||
"aitable\u0000+datasource-get-config": {},
|
||||
"aitable\u0000+datasource-get-fields": {},
|
||||
"aitable\u0000+datasource-list-sources": {},
|
||||
"aitable\u0000+datasource-sync": {},
|
||||
"aitable\u0000+datasource-sync-status": {},
|
||||
"aitable\u0000+datasource-update": {},
|
||||
"aitable\u0000+export-data": {},
|
||||
"aitable\u0000+field-delete": {},
|
||||
"aitable\u0000+field-get": {},
|
||||
@@ -375,6 +382,7 @@ func generatedPublicShortcutCatalog() map[string]struct{} {
|
||||
"minutes\u0000+upload": {},
|
||||
"minutes\u0000+upload-and-analyze": {},
|
||||
"oa\u0000+search-forms": {},
|
||||
"pat\u0000+browser-policy": {},
|
||||
"report\u0000+inbox-list": {},
|
||||
"report\u0000+outbox-list": {},
|
||||
"report\u0000+report-latest": {},
|
||||
|
||||
@@ -64,6 +64,15 @@ var sheetSemanticCatalogJSON []byte
|
||||
//go:embed semantic_catalog_whiteboard.json
|
||||
var whiteboardSemanticCatalogJSON []byte
|
||||
|
||||
//go:embed semantic_catalog_devdoc.json
|
||||
var devdocSemanticCatalogJSON []byte
|
||||
|
||||
//go:embed semantic_catalog_hrbrain.json
|
||||
var hrbrainSemanticCatalogJSON []byte
|
||||
|
||||
//go:embed semantic_catalog_pat.json
|
||||
var patSemanticCatalogJSON []byte
|
||||
|
||||
type semanticCatalogFile struct {
|
||||
Version int `json:"version"`
|
||||
Service string `json:"service"`
|
||||
@@ -101,6 +110,9 @@ var reviewedSemanticCatalog = mustLoadSemanticCatalogs(
|
||||
reportSemanticCatalogJSON,
|
||||
sheetSemanticCatalogJSON,
|
||||
whiteboardSemanticCatalogJSON,
|
||||
devdocSemanticCatalogJSON,
|
||||
hrbrainSemanticCatalogJSON,
|
||||
patSemanticCatalogJSON,
|
||||
)
|
||||
|
||||
func mustLoadSemanticCatalogs(sources ...[]byte) map[string]semanticCatalogRecord {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 1,
|
||||
"service": "devdoc",
|
||||
"default_availability": "unavailable",
|
||||
"shortcuts": {
|
||||
"+search-docs": {
|
||||
"disposition": "schema_leaf",
|
||||
"semantic_delta": "严格验证 success、result.items、文档 URL 身份和分页一致性;但 exact 与 raw 对运行时随机 zero-match 候选都返回语义近邻,无法满足 public list/search 的保证零命中证明,因此保持 unavailable。",
|
||||
"risk": "read",
|
||||
"availability": "unavailable",
|
||||
"public": false,
|
||||
"reviewed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"service": "hrbrain",
|
||||
"default_availability": "unavailable",
|
||||
"shortcuts": {
|
||||
"+list-pools": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;raw 明确 success=true/result=null,exact fail-closed,无法证明列表成功或合法空。","risk":"read","public":false,"reviewed":true},
|
||||
"+get-pool": {"disposition":"schema_leaf","semantic_delta":"classified=tenant_fixture;exact/raw 到达同一远端操作与错误,但缺少安全非空 pool fixture,不能验证同 ID 详情。","risk":"read","public":false,"reviewed":true},
|
||||
"+list-pool-employees": {"disposition":"semantic_adapter","semantic_delta":"classified=tenant_fixture;exact/raw 到达同一远端操作与错误,但缺少安全非空人才池与员工 fixture,分页合同未获 live 证明。","risk":"read","public":false,"reviewed":true},
|
||||
"+profile-metadata": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,档案元数据能力仍未解锁。","risk":"read","public":false,"reviewed":true},
|
||||
"+query-profile": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,档案查询业务链仍未解锁。","risk":"read","public":false,"reviewed":true},
|
||||
"+profile-labels": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;raw 明确 success=true/result=null,exact fail-closed,不能把未知结果投成空标签。","risk":"read","public":false,"reviewed":true},
|
||||
"+profile-career": {"disposition":"schema_leaf","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,职业历程读取仍未解锁。","risk":"read","public":false,"reviewed":true},
|
||||
"+profile-performance": {"disposition":"schema_leaf","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,且敏感绩效数据缺少受控 live 证明。","risk":"read","public":false,"reviewed":true},
|
||||
"+search-employees": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,无法完成已知非空与保证零命中证明。","risk":"read","public":false,"reviewed":true},
|
||||
"+search-employees-structured": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;exact/raw 的远端 server、operation、upstream code 一致,字段发现与结构化搜索链仍未解锁。","risk":"read","public":false,"reviewed":true},
|
||||
"+search-fields": {"disposition":"semantic_adapter","semantic_delta":"classified=adapter_business_service;无资源输入的 exact/raw 仍返回同一远端错误,字段发现能力未解锁。","risk":"read","public":false,"reviewed":true}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": 1,
|
||||
"service": "pat",
|
||||
"default_availability": "unavailable",
|
||||
"shortcuts": {
|
||||
"+browser-policy": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "在隔离本地策略文件上提供显式确认、无写入请求预览、同目标磁盘读回和不暴露 agent identity 的统一结果;exact 写入与清理已通过。",
|
||||
"risk": "write",
|
||||
"availability": "available",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
"+authorize": {
|
||||
"disposition": "alias_internal",
|
||||
"semantic_delta": "classified=routed;pat chmod 已持有 plan、fallback、pending、逐 scope ledger、身份校验与确认链,+authorize 不复制执行语义;真实授权写仍因 blocked_fixture 未获证明。",
|
||||
"risk": "high-risk-write",
|
||||
"primary": "pat chmod",
|
||||
"public": false,
|
||||
"reviewed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ SEMANTIC_PATHS = [
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_report.json",
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_sheet.json",
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_whiteboard.json",
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_devdoc.json",
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_hrbrain.json",
|
||||
ROOT / "internal" / "shortcut" / "semantic_catalog_pat.json",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ SERVICE_TO_SKILL = {
|
||||
"mail": ROOT / "skills" / "multi" / "dingtalk-mail" / "SKILL.md",
|
||||
"minutes": ROOT / "skills" / "multi" / "dingtalk-minutes" / "SKILL.md",
|
||||
"oa": ROOT / "skills" / "multi" / "dingtalk-misc" / "references" / "oa.md",
|
||||
"pat": ROOT / "skills" / "multi" / "dingtalk-misc" / "references" / "pat.md",
|
||||
"report": ROOT / "skills" / "multi" / "dingtalk-misc" / "references" / "report.md",
|
||||
"sheet": ROOT / "skills" / "multi" / "dingtalk-misc" / "references" / "sheet.md",
|
||||
"todo": ROOT / "skills" / "multi" / "dingtalk-todo" / "SKILL.md",
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
MANIFEST="$ROOT/scripts/policy/shortcut-public-e2e-proof.json"
|
||||
BINARY="${DWS_SHORTCUT_PROOF_BINARY:-$ROOT/dws}"
|
||||
GO_BINARY="${GO:-go}"
|
||||
PROOF_ROOT=""
|
||||
|
||||
fail() {
|
||||
printf 'shortcut public E2E proof failed: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$PROOF_ROOT" ] && [ -d "$PROOF_ROOT" ]; then
|
||||
find "$PROOF_ROOT" -type f -delete 2>/dev/null || true
|
||||
find "$PROOF_ROOT" -depth -type d -exec rmdir -- {} \; 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
command -v jq >/dev/null 2>&1 || fail "jq is required"
|
||||
command -v "$GO_BINARY" >/dev/null 2>&1 || fail "Go is required"
|
||||
[ -x "$BINARY" ] || fail "current source binary is missing; run make build"
|
||||
[ -f "$MANIFEST" ] || fail "reviewed proof manifest is missing"
|
||||
|
||||
if [ "${DWS_SHORTCUT_PROOF_ALLOW_DIRTY:-0}" != "1" ] &&
|
||||
[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=all)" ]; then
|
||||
fail "worktree must be clean so evidence is bound to current HEAD"
|
||||
fi
|
||||
|
||||
HEAD_SHA="$(git -C "$ROOT" rev-parse HEAD)"
|
||||
[ -n "$HEAD_SHA" ] || fail "cannot resolve current HEAD"
|
||||
|
||||
jq -e '
|
||||
.version == 1 and
|
||||
(.products | type == "array" and length > 0) and
|
||||
(.public_shortcuts | type == "array") and
|
||||
(.products | group_by(.service) | all(.[]; length == 1)) and
|
||||
(.public_shortcuts | group_by([.service, .command]) | all(.[]; length == 1)) and
|
||||
(.products | all(.[];
|
||||
(.service | type == "string" and length > 0) and
|
||||
(.source_total | type == "number") and
|
||||
(.public | type == "number") and
|
||||
(.unavailable | type == "number") and
|
||||
(.source_total == (.public + .unavailable)))) and
|
||||
(.public_shortcuts | all(.[];
|
||||
(.service | type == "string" and length > 0) and
|
||||
(.command | type == "string" and startswith("+")) and
|
||||
(.owning_atomic | type == "string" and length > 0) and
|
||||
(.proof_class | IN("list", "search", "read", "write")) and
|
||||
(.proof_case | type == "string" and length > 0) and
|
||||
(.stable_target | type == "string" and length > 0) and
|
||||
(.pass_labels | type == "array" and length > 0 and all(.[]; type == "string" and endswith("-PASS")))))
|
||||
' "$MANIFEST" >/dev/null || fail "reviewed proof manifest is malformed"
|
||||
|
||||
SOURCE_TOTAL=0
|
||||
PUBLIC_TOTAL=0
|
||||
UNAVAILABLE_TOTAL=0
|
||||
SEMANTIC_PUBLIC=""
|
||||
RUNTIME_PUBLIC=""
|
||||
|
||||
for service in $(jq -r '.products[].service' "$MANIFEST"); do
|
||||
catalog="$ROOT/internal/shortcut/semantic_catalog_${service}.json"
|
||||
[ -f "$catalog" ] || fail "semantic catalog is missing for $service"
|
||||
|
||||
jq -e --arg service "$service" '
|
||||
. as $catalog |
|
||||
.service == $service and
|
||||
.default_availability == "unavailable" and
|
||||
(.shortcuts | type == "object") and
|
||||
(.shortcuts | to_entries | all(.[];
|
||||
(.key | type == "string" and startswith("+")) and
|
||||
(.value | type == "object") and
|
||||
(.value.reviewed == true) and
|
||||
((.value.semantic_delta | type) == "string" and (.value.semantic_delta | length) > 0) and
|
||||
(if (.value.public // false)
|
||||
then ((.value.availability // $catalog.default_availability) == "available")
|
||||
else ((.value.availability // $catalog.default_availability) == "unavailable")
|
||||
end)))
|
||||
' "$catalog" >/dev/null || fail "$service semantic catalog is not truthfully reviewed"
|
||||
|
||||
source_count="$(jq '.shortcuts | length' "$catalog")"
|
||||
public_count="$(jq '[. as $catalog | .shortcuts | to_entries[] | select((.value.public // false) and ((.value.availability // $catalog.default_availability) == "available"))] | length' "$catalog")"
|
||||
unavailable_count=$((source_count - public_count))
|
||||
expected_source="$(jq -r --arg service "$service" '.products[] | select(.service == $service) | .source_total' "$MANIFEST")"
|
||||
expected_public="$(jq -r --arg service "$service" '.products[] | select(.service == $service) | .public' "$MANIFEST")"
|
||||
expected_unavailable="$(jq -r --arg service "$service" '.products[] | select(.service == $service) | .unavailable' "$MANIFEST")"
|
||||
[ "$source_count" -eq "$expected_source" ] || fail "$service source count drifted"
|
||||
[ "$public_count" -eq "$expected_public" ] || fail "$service public count drifted"
|
||||
[ "$unavailable_count" -eq "$expected_unavailable" ] || fail "$service unavailable count drifted"
|
||||
|
||||
semantic_lines="$(jq -r --arg service "$service" '. as $catalog | .shortcuts | to_entries[] | select((.value.public // false) and ((.value.availability // $catalog.default_availability) == "available")) | [$service, .key] | @tsv' "$catalog")"
|
||||
if [ -n "$semantic_lines" ]; then
|
||||
SEMANTIC_PUBLIC="${SEMANTIC_PUBLIC}${semantic_lines}"$'\n'
|
||||
fi
|
||||
|
||||
runtime_output="$($BINARY shortcut list --service "$service" --format json)"
|
||||
printf '%s' "$runtime_output" | jq -e --arg service "$service" '
|
||||
type == "object" and
|
||||
(.count | type == "number") and
|
||||
(.shortcuts | type == "array") and
|
||||
(.count == (.shortcuts | length)) and
|
||||
(.shortcuts | all(.[];
|
||||
type == "object" and
|
||||
.service == $service and
|
||||
(.command | type == "string" and startswith("+")) and
|
||||
.public == true and
|
||||
.reviewed == true and
|
||||
.availability == "available"))
|
||||
' >/dev/null || fail "$service runtime public inventory is malformed"
|
||||
runtime_count="$(printf '%s' "$runtime_output" | jq '.shortcuts | length')"
|
||||
[ "$runtime_count" -eq "$public_count" ] || fail "$service runtime public count does not match its semantic catalog"
|
||||
runtime_lines="$(printf '%s' "$runtime_output" | jq -r '.shortcuts[] | [.service, .command] | @tsv')"
|
||||
if [ -n "$runtime_lines" ]; then
|
||||
RUNTIME_PUBLIC="${RUNTIME_PUBLIC}${runtime_lines}"$'\n'
|
||||
fi
|
||||
|
||||
SOURCE_TOTAL=$((SOURCE_TOTAL + source_count))
|
||||
PUBLIC_TOTAL=$((PUBLIC_TOTAL + public_count))
|
||||
UNAVAILABLE_TOTAL=$((UNAVAILABLE_TOTAL + unavailable_count))
|
||||
done
|
||||
|
||||
SEMANTIC_PUBLIC="$(printf '%s' "$SEMANTIC_PUBLIC" | sed '/^$/d' | LC_ALL=C sort)"
|
||||
RUNTIME_PUBLIC="$(printf '%s' "$RUNTIME_PUBLIC" | sed '/^$/d' | LC_ALL=C sort)"
|
||||
EXPECTED_PUBLIC="$(jq -r '.public_shortcuts[] | [.service, .command] | @tsv' "$MANIFEST" | LC_ALL=C sort)"
|
||||
[ "$SEMANTIC_PUBLIC" = "$RUNTIME_PUBLIC" ] || fail "semantic and runtime public surfaces differ"
|
||||
[ "$RUNTIME_PUBLIC" = "$EXPECTED_PUBLIC" ] || fail "a public shortcut lacks a reviewed exact plus raw proof case"
|
||||
|
||||
printf 'CURRENT-HEAD-PUBLIC-SURFACE-PASS head=%s source=%d public=%d unavailable=%d\n' "$HEAD_SHA" "$SOURCE_TOTAL" "$PUBLIC_TOTAL" "$UNAVAILABLE_TOTAL"
|
||||
|
||||
(cd "$ROOT" && DWS_PACKAGE_VERSION="${DWS_PACKAGE_VERSION:-0.0.0-test}" "$GO_BINARY" test -count=1 ./internal/shortcut/pat -run '^TestCrossPlatformCoveragePATBrowserPolicyConfirmationDryRunReadbackAndCleanup$' >/dev/null) ||
|
||||
fail "PAT exact/raw zero-call structural proof failed"
|
||||
|
||||
run_pat_browser_policy_default() {
|
||||
schema_output="$($BINARY schema --cli-path 'pat +browser-policy' --compact --format json)"
|
||||
printf '%s' "$schema_output" | jq -e '
|
||||
type == "object" and
|
||||
.cli_path == "pat +browser-policy" and
|
||||
.effect == "write" and
|
||||
.risk == "medium" and
|
||||
.confirmation == "user_required" and
|
||||
.idempotency == "idempotent" and
|
||||
.availability == "available" and
|
||||
.interface_mode == "composite" and
|
||||
(.result | type == "object") and
|
||||
(.result.outcomes | type == "array" and index("success") != null and index("failure") != null) and
|
||||
(.result.data_schema.required | sort == ["executed", "openBrowser", "scope", "verified"])
|
||||
' >/dev/null || fail "pat +browser-policy compact contract is incomplete"
|
||||
|
||||
PROOF_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-pat-public-proof.XXXXXX")"
|
||||
exact_dir="$PROOF_ROOT/exact"
|
||||
raw_dir="$PROOF_ROOT/raw"
|
||||
mkdir -p "$exact_dir" "$raw_dir"
|
||||
exact_path="$exact_dir/pat_policy.json"
|
||||
raw_path="$raw_dir/pat_policy.json"
|
||||
|
||||
if env -u DINGTALK_DWS_AGENTCODE -u DINGTALK_DWS_SESSIONID -u DINGTALK_DWS_ORGID -u DINGTALK_DWS_UID \
|
||||
DWS_CONFIG_DIR="$exact_dir" HTTPS_PROXY=http://127.0.0.1:1 HTTP_PROXY=http://127.0.0.1:1 \
|
||||
"$BINARY" pat +browser-policy --enabled=false --format json </dev/null >/dev/null 2>&1; then
|
||||
fail "pat +browser-policy wrote without confirmation"
|
||||
fi
|
||||
[ ! -e "$exact_path" ] || fail "pat +browser-policy left state before confirmation"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-PRECONFIRM-ZERO-CALL-PASS'
|
||||
|
||||
exact_dry_run="$(env -u DINGTALK_DWS_AGENTCODE -u DINGTALK_DWS_SESSIONID -u DINGTALK_DWS_ORGID -u DINGTALK_DWS_UID \
|
||||
DWS_CONFIG_DIR="$exact_dir" HTTPS_PROXY=http://127.0.0.1:1 HTTP_PROXY=http://127.0.0.1:1 \
|
||||
"$BINARY" pat +browser-policy --enabled=false --dry-run --format json)"
|
||||
printf '%s' "$exact_dry_run" | jq -e '
|
||||
type == "object" and .ok == true and .outcome == "success" and
|
||||
(.data | type == "object") and .data.scope == "default" and
|
||||
.data.openBrowser == false and .data.executed == false and .data.verified == false
|
||||
' >/dev/null || fail "pat +browser-policy dry-run result is malformed"
|
||||
[ ! -e "$exact_path" ] || fail "pat +browser-policy dry-run wrote policy state"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-DRY-RUN-ZERO-WRITE-PASS'
|
||||
|
||||
exact_output="$(env -u DINGTALK_DWS_AGENTCODE -u DINGTALK_DWS_SESSIONID -u DINGTALK_DWS_ORGID -u DINGTALK_DWS_UID \
|
||||
DWS_CONFIG_DIR="$exact_dir" HTTPS_PROXY=http://127.0.0.1:1 HTTP_PROXY=http://127.0.0.1:1 \
|
||||
"$BINARY" pat +browser-policy --enabled=false --yes --format json)"
|
||||
printf '%s' "$exact_output" | jq -e '
|
||||
type == "object" and .ok == true and .outcome == "success" and
|
||||
(.data | type == "object") and .data.scope == "default" and
|
||||
.data.openBrowser == false and .data.executed == true and .data.verified == true
|
||||
' >/dev/null || fail "pat +browser-policy terminal receipt is malformed"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-EXACT-TERMINAL-RECEIPT-PASS'
|
||||
|
||||
jq -e '
|
||||
type == "object" and (.default | type == "object") and
|
||||
.default.openBrowser == false and ((has("agents") | not) or .agents == {})
|
||||
' "$exact_path" >/dev/null || fail "pat +browser-policy exact target readback failed"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-EXACT-READBACK-PASS'
|
||||
|
||||
raw_output="$(env -u DINGTALK_DWS_AGENTCODE -u DINGTALK_DWS_SESSIONID -u DINGTALK_DWS_ORGID -u DINGTALK_DWS_UID \
|
||||
DWS_CONFIG_DIR="$raw_dir" HTTPS_PROXY=http://127.0.0.1:1 HTTP_PROXY=http://127.0.0.1:1 \
|
||||
"$BINARY" pat browser-policy --enabled=false --format json)"
|
||||
printf '%s' "$raw_output" | jq -e '
|
||||
type == "object" and .scope == "default" and .source == "default" and
|
||||
.openBrowser == false and (has("agentCode") | not)
|
||||
' >/dev/null || fail "pat browser-policy terminal receipt is malformed"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-RAW-TERMINAL-RECEIPT-PASS'
|
||||
|
||||
jq -e '
|
||||
type == "object" and (.default | type == "object") and
|
||||
.default.openBrowser == false and ((has("agents") | not) or .agents == {})
|
||||
' "$raw_path" >/dev/null || fail "pat browser-policy raw target readback failed"
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-RAW-READBACK-PASS'
|
||||
|
||||
unlink "$exact_path"
|
||||
unlink "$raw_path"
|
||||
[ ! -e "$exact_path" ] || fail "pat +browser-policy cleanup left exact residue"
|
||||
[ ! -e "$raw_path" ] || fail "pat browser-policy cleanup left raw residue"
|
||||
find "$exact_dir" "$raw_dir" -type f -delete
|
||||
[ -z "$(find "$PROOF_ROOT" -type f -print -quit)" ] || fail "PAT browser policy proof left file residue"
|
||||
find "$PROOF_ROOT" -depth -type d -exec rmdir -- {} \;
|
||||
[ ! -e "$PROOF_ROOT" ] || fail "PAT browser policy proof left directory residue"
|
||||
PROOF_ROOT=""
|
||||
printf '%s\n' 'PAT-BROWSER-POLICY-CLEANUP-ABSENCE-PASS'
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r service command proof_case; do
|
||||
[ -n "$service" ] || continue
|
||||
case "$service $command $proof_case" in
|
||||
'pat +browser-policy pat_browser_policy_default')
|
||||
proof_output="$(run_pat_browser_policy_default)"
|
||||
;;
|
||||
*)
|
||||
fail "$service $command has no executable proof implementation"
|
||||
;;
|
||||
esac
|
||||
actual_labels="$(printf '%s\n' "$proof_output" | sed '/^$/d' | LC_ALL=C sort)"
|
||||
expected_labels="$(jq -r --arg service "$service" --arg command "$command" '.public_shortcuts[] | select(.service == $service and .command == $command) | .pass_labels[]' "$MANIFEST" | LC_ALL=C sort)"
|
||||
[ "$actual_labels" = "$expected_labels" ] || fail "$service $command did not emit every reviewed PASS label"
|
||||
printf '%s\n' "$proof_output"
|
||||
done < <(jq -r '.public_shortcuts[] | [.service, .command, .proof_case] | @tsv' "$MANIFEST")
|
||||
|
||||
printf 'EVERY-PUBLIC-SHORTCUT-DOUBLE-LAYER-E2E-PASS total=%d\n' "$PUBLIC_TOTAL"
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": 1,
|
||||
"products": [
|
||||
{
|
||||
"service": "devdoc",
|
||||
"source_total": 1,
|
||||
"public": 0,
|
||||
"unavailable": 1
|
||||
},
|
||||
{
|
||||
"service": "hrbrain",
|
||||
"source_total": 11,
|
||||
"public": 0,
|
||||
"unavailable": 11
|
||||
},
|
||||
{
|
||||
"service": "pat",
|
||||
"source_total": 2,
|
||||
"public": 1,
|
||||
"unavailable": 1
|
||||
}
|
||||
],
|
||||
"public_shortcuts": [
|
||||
{
|
||||
"service": "pat",
|
||||
"command": "+browser-policy",
|
||||
"owning_atomic": "pat browser-policy",
|
||||
"proof_class": "write",
|
||||
"proof_case": "pat_browser_policy_default",
|
||||
"stable_target": "default-policy",
|
||||
"pass_labels": [
|
||||
"PAT-BROWSER-POLICY-PRECONFIRM-ZERO-CALL-PASS",
|
||||
"PAT-BROWSER-POLICY-DRY-RUN-ZERO-WRITE-PASS",
|
||||
"PAT-BROWSER-POLICY-EXACT-TERMINAL-RECEIPT-PASS",
|
||||
"PAT-BROWSER-POLICY-RAW-TERMINAL-RECEIPT-PASS",
|
||||
"PAT-BROWSER-POLICY-EXACT-READBACK-PASS",
|
||||
"PAT-BROWSER-POLICY-RAW-READBACK-PASS",
|
||||
"PAT-BROWSER-POLICY-CLEANUP-ABSENCE-PASS"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -44,7 +44,7 @@ cli_version: ">=1.0.15"
|
||||
| 服务 | shortcut 数 | multi skill |
|
||||
|---|---:|---|
|
||||
| `aisearch` | 1 | `—` |
|
||||
| `aitable` | 93 | `dingtalk-aitable` |
|
||||
| `aitable` | 100 | `dingtalk-aitable` |
|
||||
| `attendance` | 8 | `dingtalk-misc` |
|
||||
| `calendar` | 27 | `dingtalk-calendar` |
|
||||
| `chat` | 98 | `dingtalk-chat` |
|
||||
@@ -56,6 +56,7 @@ cli_version: ">=1.0.15"
|
||||
| `mail` | 8 | `dingtalk-mail` |
|
||||
| `minutes` | 27 | `dingtalk-minutes` |
|
||||
| `oa` | 1 | `dingtalk-misc` |
|
||||
| `pat` | 1 | `dingtalk-misc` |
|
||||
| `report` | 4 | `dingtalk-misc` |
|
||||
| `sheet` | 2 | `dingtalk-misc` |
|
||||
| `todo` | 21 | `dingtalk-todo` |
|
||||
|
||||
@@ -28,7 +28,7 @@ metadata:
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
`aitable` 当前有 93 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
`aitable` 当前有 100 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
|
||||
`dws pat` 管理 Agent 的行为授权。它不管理开放平台应用权限;应用权限使用 `dws dev app permission`(见 [devapp.md](./devapp.md))。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcuts(无专用脚本/recipe 时优先)
|
||||
|
||||
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 Agent leaf Schema(例如 `dws schema --cli-path "pat +<shortcut>" --compact --format json`),在当前 Cobra flags 不确定时读取 `dws pat <shortcut> --help`。只有参数映射、接口绑定或 provenance 审计才省略 `--compact`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service pat --format json` 批量发现。
|
||||
|
||||
| Shortcut | 风险 | 适用场景 |
|
||||
|---|---|---|
|
||||
| `dws pat +browser-policy` | write | 安全配置 PAT 授权时是否允许打开本地浏览器 |
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 配置浏览器策略
|
||||
|
||||
Reference in New Issue
Block a user