Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91af2bc3b8 | ||
|
|
e2e8b3bf52 | ||
|
|
a652b90fd4 |
@@ -53,6 +53,7 @@ __KEG_ONLY_LINE__
|
||||
Pathname.new(File.join(Dir.home, ".kiro/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".trae/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".openclaw/skills/dws")),
|
||||
Pathname.new(File.join(Dir.home, ".hermes/skills/dws")),
|
||||
]
|
||||
|
||||
targets.each_with_index do |dest, index|
|
||||
|
||||
@@ -22,6 +22,7 @@ const AGENT_DIRS = [
|
||||
".kiro/skills",
|
||||
".trae/skills",
|
||||
".openclaw/skills",
|
||||
".hermes/skills",
|
||||
];
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
|
||||
@@ -227,14 +227,11 @@ func directRuntimeEndpoint(productID, toolName string) (string, bool) {
|
||||
te := dynamicToolEndpoints
|
||||
dynamicMu.RUnlock()
|
||||
|
||||
// Priority 1: tool-level endpoint (resolves multi-endpoint products).
|
||||
if tool := strings.TrimSpace(toolName); tool != "" && te != nil {
|
||||
if endpoint, ok := te[tool]; ok {
|
||||
return endpoint, true
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: product-level endpoint.
|
||||
// Priority 1: product-level endpoint.
|
||||
// When the caller already knows the productID (e.g. "drive"), the product
|
||||
// endpoint is authoritative. This prevents cross-product tool name
|
||||
// collisions (e.g. both "drive" and "doc" register "create_folder") from
|
||||
// routing the request to the wrong MCP server. See issue #219.
|
||||
for _, candidate := range []string{strings.TrimSpace(productID), normalized} {
|
||||
if candidate == "" {
|
||||
continue
|
||||
@@ -246,6 +243,16 @@ func directRuntimeEndpoint(productID, toolName string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: tool-level endpoint (fallback for unknown productID).
|
||||
// This path is used when the caller does not know the productID but has a
|
||||
// tool name, e.g. in helper invocations or plugin routes where only the
|
||||
// tool name is available.
|
||||
if tool := strings.TrimSpace(toolName); tool != "" && te != nil {
|
||||
if endpoint, ok := te[tool]; ok {
|
||||
return endpoint, true
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: built-in PAT fallback for cold-start paths that run before
|
||||
// discovery/plugin registration has populated the dynamic registry.
|
||||
for _, candidate := range []string{strings.TrimSpace(productID), normalized} {
|
||||
|
||||
@@ -181,3 +181,114 @@ func TestAppendDynamicServer_ServerOverrideDoesNotHijackToolEndpoint(t *testing.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Issue #219 regression tests: cross-product tool name collision ---
|
||||
//
|
||||
// When two different products register tools with the same name (e.g. drive
|
||||
// and doc both have "create_folder"), the product-level endpoint must win
|
||||
// when the caller already knows the productID. Otherwise the tool-level map
|
||||
// (last-writer-wins) routes the invocation to the wrong MCP server.
|
||||
|
||||
const (
|
||||
testDriveEndpoint = "https://mcp-gw.dingtalk.com/server/drive-hash"
|
||||
testDocEndpoint = "https://mcp-gw.dingtalk.com/server/doc-hash"
|
||||
)
|
||||
|
||||
func driveDescriptor() market.ServerDescriptor {
|
||||
return market.ServerDescriptor{
|
||||
Endpoint: testDriveEndpoint,
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "drive",
|
||||
Command: "drive",
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"create_folder": {CLIName: "mkdir"},
|
||||
"list_files": {CLIName: "list"},
|
||||
"download_file": {CLIName: "download"},
|
||||
"get_upload_info": {CLIName: "upload-info"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func docDescriptor() market.ServerDescriptor {
|
||||
return market.ServerDescriptor{
|
||||
Endpoint: testDocEndpoint,
|
||||
CLI: market.CLIOverlay{
|
||||
ID: "doc",
|
||||
Command: "doc",
|
||||
ToolOverrides: map[string]market.CLIToolOverride{
|
||||
"create_folder": {CLIName: "create", Group: "folder"},
|
||||
"download_file": {CLIName: "download"},
|
||||
"search_documents": {CLIName: "search"},
|
||||
"list_nodes": {CLIName: "list"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirectRuntimeEndpoint_ProductLevelWinsOverConflictingToolLevel verifies
|
||||
// that when productID is known and has a registered endpoint, the product-level
|
||||
// endpoint is used even if the tool-level map points to a different server
|
||||
// (due to same-name tool collision). This is the core fix for issue #219.
|
||||
func TestDirectRuntimeEndpoint_ProductLevelWinsOverConflictingToolLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
servers []market.ServerDescriptor
|
||||
}{
|
||||
{
|
||||
name: "drive first, doc second",
|
||||
servers: []market.ServerDescriptor{driveDescriptor(), docDescriptor()},
|
||||
},
|
||||
{
|
||||
name: "doc first, drive second",
|
||||
servers: []market.ServerDescriptor{docDescriptor(), driveDescriptor()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
withCleanDynamicRegistry(t)
|
||||
SetDynamicServers(tc.servers)
|
||||
|
||||
// Drive tools must always route to drive's endpoint regardless of
|
||||
// registration order — productID "drive" is known.
|
||||
assertEndpoint(t, "drive", "create_folder", testDriveEndpoint)
|
||||
assertEndpoint(t, "drive", "download_file", testDriveEndpoint)
|
||||
assertEndpoint(t, "drive", "list_files", testDriveEndpoint)
|
||||
assertEndpoint(t, "drive", "get_upload_info", testDriveEndpoint)
|
||||
|
||||
// Doc tools must always route to doc's endpoint.
|
||||
assertEndpoint(t, "doc", "create_folder", testDocEndpoint)
|
||||
assertEndpoint(t, "doc", "download_file", testDocEndpoint)
|
||||
assertEndpoint(t, "doc", "search_documents", testDocEndpoint)
|
||||
assertEndpoint(t, "doc", "list_nodes", testDocEndpoint)
|
||||
|
||||
// Product-level fallback (no tool name) still works.
|
||||
assertEndpoint(t, "drive", "", testDriveEndpoint)
|
||||
assertEndpoint(t, "doc", "", testDocEndpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirectRuntimeEndpoint_ToolLevelFallbackWhenProductUnknown verifies that
|
||||
// tool-level routing still works as a fallback when productID is empty or has
|
||||
// no registered endpoint (the original design intent for tool-level Priority 1).
|
||||
func TestDirectRuntimeEndpoint_ToolLevelFallbackWhenProductUnknown(t *testing.T) {
|
||||
withCleanDynamicRegistry(t)
|
||||
SetDynamicServers([]market.ServerDescriptor{driveDescriptor(), docDescriptor()})
|
||||
|
||||
// When productID is empty, tool-level endpoint is the only option.
|
||||
// The actual endpoint depends on registration order (last-writer-wins),
|
||||
// but the lookup must succeed.
|
||||
endpoint, ok := directRuntimeEndpoint("", "create_folder")
|
||||
if !ok {
|
||||
t.Fatal("directRuntimeEndpoint(\"\", \"create_folder\") returned ok=false, want ok=true")
|
||||
}
|
||||
if endpoint != testDriveEndpoint && endpoint != testDocEndpoint {
|
||||
t.Fatalf("directRuntimeEndpoint(\"\", \"create_folder\") = %q, want one of drive/doc endpoints", endpoint)
|
||||
}
|
||||
|
||||
// Unique tools (no collision) still resolve via tool-level.
|
||||
assertEndpoint(t, "", "search_documents", testDocEndpoint)
|
||||
assertEndpoint(t, "", "get_upload_info", testDriveEndpoint)
|
||||
}
|
||||
|
||||
@@ -268,15 +268,20 @@ func newAttendanceShiftListCommand(runner executor.Runner) *cobra.Command {
|
||||
|
||||
func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "查询某个人的考勤统计摘要",
|
||||
Long: "查询某个人的考勤统计摘要。--user 与 --date 均必填。",
|
||||
Example: ` dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"`,
|
||||
Use: "summary",
|
||||
Short: "查询某个人的考勤统计摘要",
|
||||
Long: `查询某个人的考勤统计摘要。
|
||||
|
||||
--user、--date、--stats-type 均必填。
|
||||
钉钉服务端业务层强制要求 --stats-type(week/month),不填会返回 C0002 统计类型错误。`,
|
||||
Example: ` dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type week`,
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
userID, _ := cmd.Flags().GetString("user")
|
||||
workDateStr, _ := cmd.Flags().GetString("date")
|
||||
statsType, _ := cmd.Flags().GetString("stats-type")
|
||||
if userID == "" {
|
||||
return apperrors.NewValidation("--user is required, provide DingTalk user ID")
|
||||
}
|
||||
@@ -287,10 +292,17 @@ func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
if err != nil {
|
||||
return apperrors.NewValidation("--date format error, use yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
if statsType == "" {
|
||||
return apperrors.NewValidation(`--stats-type is required (week|month), enforced by DingTalk server`)
|
||||
}
|
||||
if statsType != "week" && statsType != "month" {
|
||||
return apperrors.NewValidation(`--stats-type must be "week" or "month"`)
|
||||
}
|
||||
// Build nested structure QueryUserAttendVO
|
||||
vo := map[string]any{
|
||||
"userId": userID,
|
||||
"queryDate": workDateStr,
|
||||
"statsType": statsType,
|
||||
}
|
||||
params := map[string]any{
|
||||
"QueryUserAttendVO": vo,
|
||||
@@ -311,6 +323,7 @@ func newAttendanceSummaryCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
cmd.Flags().String("user", "", "钉钉用户 ID(必填)")
|
||||
cmd.Flags().String("date", "", "工作日期,格式 yyyy-MM-dd HH:mm:ss,如 2026-03-12 15:00:00(必填)")
|
||||
cmd.Flags().String("stats-type", "", "统计类型:week(周统计)或 month(月统计)(必填,钉钉服务端业务层强制要求)")
|
||||
preferLegacyLeaf(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -19,7 +19,15 @@ const (
|
||||
)
|
||||
|
||||
// knownSkillDirs lists all known Agent skill directories (relative to $HOME).
|
||||
// Kept in sync with build/npm/install.js AGENT_DIRS.
|
||||
// Kept in sync with:
|
||||
// - build/npm/install.js AGENT_DIRS
|
||||
// - scripts/install.sh for-in list
|
||||
// - scripts/install.ps1 $AgentDirs
|
||||
// - scripts/install-skills.sh for-in list
|
||||
// - build/homebrew.rb.tmpl targets
|
||||
// - test/scripts/package_script_test.go expectedPackagedSkillTargets
|
||||
// - scripts/release/verify-package-managers.sh HOME_AGENT_PARENTS / HOME_SKILL_TARGETS
|
||||
//
|
||||
// The first entry (.agents/skills) is always updated; subsequent entries are
|
||||
// only updated when their parent directory already exists.
|
||||
var knownSkillDirs = []string{
|
||||
@@ -36,6 +44,7 @@ var knownSkillDirs = []string{
|
||||
".kiro/skills",
|
||||
".trae/skills",
|
||||
".openclaw/skills",
|
||||
".hermes/skills",
|
||||
}
|
||||
|
||||
// skillDirBlacklist contains parent directories whose skills are managed by
|
||||
|
||||
@@ -116,7 +116,8 @@ install_skills_to_root() {
|
||||
".amp/skills" \
|
||||
".kiro/skills" \
|
||||
".trae/skills" \
|
||||
".openclaw/skills"
|
||||
".openclaw/skills" \
|
||||
".hermes/skills"
|
||||
do
|
||||
base_dir="$root/$agent_dir"
|
||||
parent_gate="$(dirname "$base_dir")"
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@ $AgentDirs = @(
|
||||
".amp\skills",
|
||||
".kiro\skills",
|
||||
".trae\skills",
|
||||
".openclaw\skills"
|
||||
".openclaw\skills",
|
||||
".hermes\skills"
|
||||
)
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+2
-1
@@ -201,7 +201,8 @@ install_skills_to_homes() {
|
||||
".amp/skills" \
|
||||
".kiro/skills" \
|
||||
".trae/skills" \
|
||||
".openclaw/skills"
|
||||
".openclaw/skills" \
|
||||
".hermes/skills"
|
||||
do
|
||||
base_dir="$root/$agent_dir"
|
||||
parent_gate="$(dirname "$base_dir")"
|
||||
|
||||
@@ -37,6 +37,7 @@ HOME_AGENT_PARENTS="
|
||||
.kiro
|
||||
.trae
|
||||
.openclaw
|
||||
.hermes
|
||||
"
|
||||
HOME_SKILL_TARGETS="
|
||||
.agents/skills/dws
|
||||
@@ -52,6 +53,7 @@ HOME_SKILL_TARGETS="
|
||||
.kiro/skills/dws
|
||||
.trae/skills/dws
|
||||
.openclaw/skills/dws
|
||||
.hermes/skills/dws
|
||||
"
|
||||
cleanup() {
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
|
||||
@@ -32,12 +32,16 @@ Flags:
|
||||
Usage:
|
||||
dws attendance summary [flags]
|
||||
Example:
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00"
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type month
|
||||
dws attendance summary --user USER_ID --date "2026-03-12 15:00:00" --stats-type week
|
||||
Flags:
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
--date string 工作日期, 格式 yyyy-MM-dd HH:mm:ss (必填)
|
||||
--stats-type string 统计类型:week(周统计)或 month(月统计)(必填,钉钉服务端业务层强制要求;CLI 层会直接拒绝缺失/非法值)
|
||||
--user string 钉钉用户 ID (必填)
|
||||
```
|
||||
|
||||
> ⚠️ **重要**:`--stats-type` 在钉钉 schema 中标记为 `required: []`(看似可选),但服务端业务层**强制要求**,不传服务端会回 `C0002 / 统计类型错误`。CLI 已在客户端层做了 fail-fast:缺失或非 `week`/`month` 的取值会直接被 CLI 拒绝,不会发出请求。
|
||||
|
||||
### 查询考勤组与考勤规则
|
||||
```
|
||||
Usage:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cli_compat_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -146,7 +148,11 @@ func TestAttendanceShiftList_should_filter_empty_user_ids(t *testing.T) {
|
||||
func TestAttendanceSummary_should_call_tool_with_user_and_date(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U001", "date": "2026-03-12 15:00:00"})
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -157,8 +163,9 @@ func TestAttendanceSummary_should_pass_user_and_date_flags(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
last := cap.last()
|
||||
if last == nil {
|
||||
@@ -200,10 +207,14 @@ func TestAttendanceSummary_should_error_when_user_missing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_only_user_flag(t *testing.T) {
|
||||
func TestAttendanceSummary_should_pass_user_id_through_vo(t *testing.T) {
|
||||
cap := setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U002", "date": "2026-03-12 15:00:00"})
|
||||
_ = execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U002",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
last := cap.last()
|
||||
if last == nil {
|
||||
t.Fatal("no call captured")
|
||||
@@ -220,7 +231,11 @@ func TestAttendanceSummary_should_pass_only_user_flag(t *testing.T) {
|
||||
func TestAttendanceSummary_should_use_dry_run_mode(t *testing.T) {
|
||||
cap := setupTestDepsWithDryRun(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{"user": "U001", "date": "2026-03-12 15:00:00"})
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -229,6 +244,97 @@ func TestAttendanceSummary_should_use_dry_run_mode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// execSummaryDryRun runs attendance summary with --dry-run and returns the
|
||||
// parsed QueryUserAttendVO map from the helper_invocation payload. This is
|
||||
// necessary because the attendance handler is a custom helper (not a dynamic
|
||||
// MCP-route), so the test framework's mcpCallCapture cannot intercept the
|
||||
// call in non-dry-run mode.
|
||||
func execSummaryDryRun(t *testing.T, flags map[string]string) map[string]any {
|
||||
t.Helper()
|
||||
root := buildRoot()
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
|
||||
cliArgs := []string{"-f", "json", "attendance", "summary", "--dry-run"}
|
||||
for k, v := range flags {
|
||||
if v != "" {
|
||||
cliArgs = append(cliArgs, "--"+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&bytes.Buffer{})
|
||||
root.SetArgs(cliArgs)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected CLI error: %v", err)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Params map[string]any `json:"params"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("failed to parse dry-run output as JSON: %v\noutput: %s", err, out.String())
|
||||
}
|
||||
vo, ok := payload.Params["QueryUserAttendVO"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected QueryUserAttendVO map in dry-run params, got %T: %v", payload.Params["QueryUserAttendVO"], payload.Params)
|
||||
}
|
||||
return vo
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_stats_type_when_provided(t *testing.T) {
|
||||
vo := execSummaryDryRun(t, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "month",
|
||||
})
|
||||
if vo["statsType"] != "month" {
|
||||
t.Errorf("expected statsType=month in VO, got %v", vo["statsType"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_pass_stats_type_week(t *testing.T) {
|
||||
vo := execSummaryDryRun(t, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "week",
|
||||
})
|
||||
if vo["statsType"] != "week" {
|
||||
t.Errorf("expected statsType=week in VO, got %v", vo["statsType"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_error_when_stats_type_missing(t *testing.T) {
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when --stats-type is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "stats-type") {
|
||||
t.Errorf("expected error message to mention stats-type, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttendanceSummary_should_error_when_stats_type_invalid(t *testing.T) {
|
||||
_ = setupTestDeps(t, "attendance")
|
||||
root := buildRoot()
|
||||
err := execCmd(t, root, []string{"attendance", "summary"}, map[string]string{
|
||||
"user": "U001",
|
||||
"date": "2026-03-12 15:00:00",
|
||||
"stats-type": "foobar",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when --stats-type is neither week nor month")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "week") || !strings.Contains(err.Error(), "month") {
|
||||
t.Errorf("expected error message to mention week and month, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── attendance rules ───────────────────────────────────────
|
||||
|
||||
func TestAttendanceRules_should_call_tool_with_date(t *testing.T) {
|
||||
|
||||
@@ -23,6 +23,7 @@ var expectedPackagedSkillTargets = []string{
|
||||
".kiro/skills/dws",
|
||||
".trae/skills/dws",
|
||||
".openclaw/skills/dws",
|
||||
".hermes/skills/dws",
|
||||
}
|
||||
|
||||
// seedDistArtifacts creates fake goreleaser output archives (empty tar.gz/zip
|
||||
|
||||
Reference in New Issue
Block a user