@openfinclaw/openfinclaw-strategy 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/index.test.ts +269 -0
- package/index.ts +1005 -0
- package/openclaw.plugin.json +35 -0
- package/package.json +45 -0
- package/skills/openfinclaw/SKILL.md +301 -0
- package/skills/skill-publish/SKILL.md +316 -0
- package/skills/strategy-builder/SKILL.md +555 -0
- package/skills/strategy-fork/SKILL.md +165 -0
- package/skills/strategy-pack/SKILL.md +285 -0
- package/src/cli.ts +321 -0
- package/src/fork.ts +342 -0
- package/src/strategy-storage.test.ts +109 -0
- package/src/strategy-storage.ts +303 -0
- package/src/types.ts +494 -0
- package/src/validate.test.ts +841 -0
- package/src/validate.ts +594 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Peter Steinberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @openfinclaw/openfinclaw-strategy
|
|
2
|
+
|
|
3
|
+
FinClaw Commons - 量化策略开发平台插件。连接 hub.openfinclaw.ai 策略网络,支持策略创建、验证、发布、Fork。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -fsSL https://raw.githubusercontent.com/cryptoSUN2049/openFinclaw/main/scripts/install-finclaw.sh | bash
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或手动安装:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @openfinclaw/openfinclaw-strategy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 快速开始(无需 API Key)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 查看排行榜
|
|
21
|
+
openclaw strategy leaderboard
|
|
22
|
+
|
|
23
|
+
# 查看收益榜 Top 10
|
|
24
|
+
openclaw strategy leaderboard returns --limit 10
|
|
25
|
+
|
|
26
|
+
# 查看策略详情
|
|
27
|
+
openclaw strategy show <strategy-id> --remote
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 功能概览
|
|
31
|
+
|
|
32
|
+
| 功能 | 工具 | 需要 API Key |
|
|
33
|
+
| ---------- | ---------------------- | ------------ |
|
|
34
|
+
| 排行榜查询 | `skill_leaderboard` | 否 |
|
|
35
|
+
| 策略详情 | `skill_get_info` | 否 |
|
|
36
|
+
| 本地验证 | `skill_validate` | 否 |
|
|
37
|
+
| 本地列表 | `skill_list_local` | 否 |
|
|
38
|
+
| 策略 Fork | `skill_fork` | **是** |
|
|
39
|
+
| 策略发布 | `skill_publish` | **是** |
|
|
40
|
+
| 发布查询 | `skill_publish_verify` | **是** |
|
|
41
|
+
|
|
42
|
+
### AI 工具列表
|
|
43
|
+
|
|
44
|
+
| 工具 | 说明 | API Key |
|
|
45
|
+
| ---------------------- | -------------------------------------- | -------- |
|
|
46
|
+
| `skill_leaderboard` | 查询排行榜(综合/收益/风控/人气/新星) | 不需要 |
|
|
47
|
+
| `skill_get_info` | 获取 Hub 策略公开详情 | 不需要 |
|
|
48
|
+
| `skill_validate` | 本地验证策略包(FEP v1.2) | 不需要 |
|
|
49
|
+
| `skill_list_local` | 列出本地策略 | 不需要 |
|
|
50
|
+
| `skill_fork` | 从 Hub 下载策略到本地 | **需要** |
|
|
51
|
+
| `skill_publish` | 发布策略 ZIP 到 Hub,自动触发回测 | **需要** |
|
|
52
|
+
| `skill_publish_verify` | 查询发布状态和回测报告 | **需要** |
|
|
53
|
+
|
|
54
|
+
## CLI 命令
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 查看排行榜(无需 API Key)
|
|
58
|
+
openclaw strategy leaderboard
|
|
59
|
+
openclaw strategy leaderboard returns --limit 10
|
|
60
|
+
|
|
61
|
+
# 从 Hub Fork 策略(需要 API Key)
|
|
62
|
+
openclaw strategy fork <strategy-id>
|
|
63
|
+
|
|
64
|
+
# 列出本地策略
|
|
65
|
+
openclaw strategy list
|
|
66
|
+
|
|
67
|
+
# 查看策略详情
|
|
68
|
+
openclaw strategy show <name-or-id> [--remote]
|
|
69
|
+
|
|
70
|
+
# 删除本地策略
|
|
71
|
+
openclaw strategy remove <name-or-id> --force
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 排行榜类型
|
|
75
|
+
|
|
76
|
+
| 榜单类型 | 说明 |
|
|
77
|
+
| ----------- | -------------- |
|
|
78
|
+
| `composite` | 综合榜(默认) |
|
|
79
|
+
| `returns` | 收益榜 |
|
|
80
|
+
| `risk` | 风控榜 |
|
|
81
|
+
| `popular` | 人气榜 |
|
|
82
|
+
| `rising` | 新星榜 |
|
|
83
|
+
|
|
84
|
+
## 配置
|
|
85
|
+
|
|
86
|
+
Fork 和发布策略需要 API Key(从 https://hub.openfinclaw.ai 获取):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
openclaw config set plugins.entries.openfinclaw.config.skillApiKey YOUR_API_KEY
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
或使用环境变量:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export SKILL_API_KEY=YOUR_API_KEY
|
|
96
|
+
export SKILL_API_URL=https://hub.openfinclaw.ai
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### ⚠️ API Key 安全提醒
|
|
100
|
+
|
|
101
|
+
**重要:请勿泄露你的 Hub API Key!**
|
|
102
|
+
|
|
103
|
+
- API Key 以 `fch_` 开头,**仅用于** hub.openfinclaw.ai 接口校验
|
|
104
|
+
- **不要**将 API Key 提交到 Git 仓库或公开分享
|
|
105
|
+
- **不要**在公开聊天、截图、代码示例中暴露真实的 API Key
|
|
106
|
+
- 如果怀疑 Key 已泄露,请立即在 Hub 个人设置中重新生成
|
|
107
|
+
|
|
108
|
+
### 配置选项
|
|
109
|
+
|
|
110
|
+
| 配置项 | 环境变量 | 说明 | 默认值 |
|
|
111
|
+
| ------------------ | -------------------------- | ---------------------------- | ---------------------------- |
|
|
112
|
+
| `skillApiKey` | `SKILL_API_KEY` | Hub API Key(Fork/发布需要) | 可选 |
|
|
113
|
+
| `skillApiUrl` | `SKILL_API_URL` | Hub 服务地址 | `https://hub.openfinclaw.ai` |
|
|
114
|
+
| `requestTimeoutMs` | `SKILL_REQUEST_TIMEOUT_MS` | 请求超时 | `60000` |
|
|
115
|
+
|
|
116
|
+
## Skills
|
|
117
|
+
|
|
118
|
+
### openfinclaw (入口)
|
|
119
|
+
|
|
120
|
+
平台入口 skill,帮助用户了解 Hub 平台、安装插件、使用工具链。
|
|
121
|
+
|
|
122
|
+
### skill-publish
|
|
123
|
+
|
|
124
|
+
发布策略到 Hub:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
用户: "发布我的策略到服务器"
|
|
128
|
+
Agent:
|
|
129
|
+
1. skill_validate(dirPath) → 本地验证
|
|
130
|
+
2. [打包 ZIP]
|
|
131
|
+
3. skill_publish(filePath) → 获取 submissionId
|
|
132
|
+
4. skill_publish_verify(submissionId) → 轮询直到完成
|
|
133
|
+
5. 返回回测报告
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### strategy-builder
|
|
137
|
+
|
|
138
|
+
自然语言生成策略:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
用户: "帮我创建一个 BTC 定投策略,每周买入 100 美元"
|
|
142
|
+
Agent:
|
|
143
|
+
1. 生成 fep.yaml 配置
|
|
144
|
+
2. 生成 scripts/strategy.py 代码
|
|
145
|
+
3. 可选:验证并打包
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### strategy-fork
|
|
149
|
+
|
|
150
|
+
从 Hub 下载策略:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
用户: "帮我下载那个收益 453% 的 BTC 策略"
|
|
154
|
+
Agent:
|
|
155
|
+
1. skill_leaderboard() → 查看排行榜
|
|
156
|
+
2. skill_get_info(strategyId) → 查看详情
|
|
157
|
+
3. skill_fork(strategyId) → 下载到本地
|
|
158
|
+
4. 返回本地路径供编辑
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 本地存储
|
|
162
|
+
|
|
163
|
+
策略存储在 `~/.openfinclaw/workspace/strategies/`:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
~/.openfinclaw/workspace/strategies/
|
|
167
|
+
└── 2026-03-16/
|
|
168
|
+
├── btc-adaptive-dca-34a5792f/ # Fork 来的策略
|
|
169
|
+
│ ├── fep.yaml
|
|
170
|
+
│ ├── scripts/strategy.py
|
|
171
|
+
│ └── .fork-meta.json
|
|
172
|
+
└── my-new-strategy/ # 自建策略
|
|
173
|
+
└── ...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 链接
|
|
177
|
+
|
|
178
|
+
- **Hub 平台**: https://hub.openfinclaw.ai
|
|
179
|
+
- **排行榜**: https://hub.openfinclaw.ai/leaderboard
|
|
180
|
+
- **获取 API Key**: https://hub.openfinclaw.ai/dashboard
|
|
181
|
+
- **GitHub**: https://github.com/cryptoSUN2049/openFinclaw
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for openfinclaw plugin: registration, config resolution, and tool execution with mocked HTTP.
|
|
3
|
+
*/
|
|
4
|
+
import type { OpenClawPluginApi } from "openfinclaw/plugin-sdk";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import plugin from "./index.js";
|
|
7
|
+
|
|
8
|
+
vi.mock("node:fs/promises", () => ({
|
|
9
|
+
readFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
const { readFile } = await import("node:fs/promises");
|
|
12
|
+
|
|
13
|
+
type Tool = {
|
|
14
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function parseResult(result: unknown): Record<string, unknown> {
|
|
18
|
+
const details = (result as { details?: Record<string, unknown> }).details;
|
|
19
|
+
if (details) return details;
|
|
20
|
+
const content = (result as { content: Array<{ text: string }> }).content?.[0]?.text;
|
|
21
|
+
if (!content) return {};
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createFakeApi(pluginConfig: Record<string, unknown>): {
|
|
30
|
+
api: OpenClawPluginApi;
|
|
31
|
+
tools: Map<string, Tool>;
|
|
32
|
+
} {
|
|
33
|
+
const tools = new Map<string, Tool>();
|
|
34
|
+
const api = {
|
|
35
|
+
id: "openfinclaw",
|
|
36
|
+
name: "OpenFinClaw",
|
|
37
|
+
source: "test",
|
|
38
|
+
config: {},
|
|
39
|
+
pluginConfig,
|
|
40
|
+
runtime: { version: "test", services: new Map() },
|
|
41
|
+
logger: { info() {}, warn() {}, error() {}, debug() {} },
|
|
42
|
+
registerTool(tool: { name: string; execute: Tool["execute"] }) {
|
|
43
|
+
tools.set(tool.name, tool);
|
|
44
|
+
},
|
|
45
|
+
registerHook() {},
|
|
46
|
+
registerHttpHandler() {},
|
|
47
|
+
registerHttpRoute() {},
|
|
48
|
+
registerChannel() {},
|
|
49
|
+
registerGatewayMethod() {},
|
|
50
|
+
registerCli() {},
|
|
51
|
+
registerService() {},
|
|
52
|
+
registerProvider() {},
|
|
53
|
+
registerCommand() {},
|
|
54
|
+
resolvePath: (p: string) => p,
|
|
55
|
+
on() {},
|
|
56
|
+
} as unknown as OpenClawPluginApi;
|
|
57
|
+
|
|
58
|
+
return { api, tools };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("openfinclaw plugin", () => {
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.unstubAllGlobals();
|
|
64
|
+
vi.unstubAllEnvs();
|
|
65
|
+
vi.restoreAllMocks();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("has correct plugin metadata", () => {
|
|
69
|
+
expect(plugin.id).toBe("openfinclaw");
|
|
70
|
+
expect(plugin.name).toBe("OpenFinClaw");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("registers all 3 tools", () => {
|
|
74
|
+
const { api, tools } = createFakeApi({});
|
|
75
|
+
plugin.register(api);
|
|
76
|
+
|
|
77
|
+
expect(tools.size).toBe(7);
|
|
78
|
+
expect(tools.has("skill_publish")).toBe(true);
|
|
79
|
+
expect(tools.has("skill_publish_verify")).toBe(true);
|
|
80
|
+
expect(tools.has("skill_validate")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("skill_publish_verify returns success when API returns 200 with submissionId", async () => {
|
|
84
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
85
|
+
ok: true,
|
|
86
|
+
status: 200,
|
|
87
|
+
text: async () =>
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
submissionId: "sub-123",
|
|
90
|
+
slug: "test-strategy",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
backtestStatus: "completed",
|
|
93
|
+
backtestCompleted: true,
|
|
94
|
+
backtestReport: { performance: { totalReturn: 0.1, sharpe: 1.2 } },
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
98
|
+
|
|
99
|
+
const { api, tools } = createFakeApi({
|
|
100
|
+
skillApiUrl: "http://skill.example",
|
|
101
|
+
skillApiKey: "fch_test_key",
|
|
102
|
+
});
|
|
103
|
+
plugin.register(api);
|
|
104
|
+
|
|
105
|
+
const result = parseResult(
|
|
106
|
+
await tools.get("skill_publish_verify")!.execute("call-1", { submissionId: "sub-123" }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
expect(result.slug).toBe("test-strategy");
|
|
111
|
+
expect(result.backtestStatus).toBe("completed");
|
|
112
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
113
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
114
|
+
expect(url).toContain("/api/v1/skill/publish/verify");
|
|
115
|
+
expect(url).toContain("submissionId=sub-123");
|
|
116
|
+
expect((init.headers as Record<string, string>)["Authorization"]).toBe("Bearer fch_test_key");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("skill_publish_verify returns success when API returns 200 with backtestTaskId", async () => {
|
|
120
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
121
|
+
ok: true,
|
|
122
|
+
status: 200,
|
|
123
|
+
text: async () =>
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
submissionId: "sub-456",
|
|
126
|
+
backtestTaskId: "bt-789",
|
|
127
|
+
backtestStatus: "processing",
|
|
128
|
+
backtestCompleted: false,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
132
|
+
|
|
133
|
+
const { api, tools } = createFakeApi({
|
|
134
|
+
skillApiUrl: "http://skill.example",
|
|
135
|
+
skillApiKey: "fch_test_key",
|
|
136
|
+
});
|
|
137
|
+
plugin.register(api);
|
|
138
|
+
|
|
139
|
+
const result = parseResult(
|
|
140
|
+
await tools.get("skill_publish_verify")!.execute("call-2", { backtestTaskId: "bt-789" }),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(result.backtestStatus).toBe("processing");
|
|
145
|
+
const [url] = fetchMock.mock.calls[0] as [string];
|
|
146
|
+
expect(url).toContain("backtestTaskId=bt-789");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("skill_publish requires filePath and apiKey", async () => {
|
|
150
|
+
const { api, tools } = createFakeApi({});
|
|
151
|
+
plugin.register(api);
|
|
152
|
+
|
|
153
|
+
const result = parseResult(
|
|
154
|
+
await tools.get("skill_publish")!.execute("call-3", { filePath: "" }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(result.success).toBe(false);
|
|
158
|
+
expect(String(result.error)).toContain("filePath is required");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("skill_publish returns error when apiKey is not configured", async () => {
|
|
162
|
+
const { api, tools } = createFakeApi({ skillApiUrl: "http://skill.example" });
|
|
163
|
+
plugin.register(api);
|
|
164
|
+
|
|
165
|
+
const result = parseResult(
|
|
166
|
+
await tools.get("skill_publish")!.execute("call-4", { filePath: "/tmp/test.zip" }),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.success).toBe(false);
|
|
170
|
+
expect(String(result.error)).toContain("API key not configured");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("skill_publish sends POST with base64 content", async () => {
|
|
174
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("fake-zip-content"));
|
|
175
|
+
|
|
176
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
177
|
+
ok: true,
|
|
178
|
+
status: 201,
|
|
179
|
+
text: async () =>
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
slug: "test-skill",
|
|
182
|
+
entryId: "entry-uuid",
|
|
183
|
+
version: "1.0.0",
|
|
184
|
+
status: "completed",
|
|
185
|
+
submissionId: "sub-new",
|
|
186
|
+
backtestTaskId: "bt-new",
|
|
187
|
+
backtestStatus: "completed",
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
191
|
+
|
|
192
|
+
const { api, tools } = createFakeApi({
|
|
193
|
+
skillApiUrl: "http://skill.example",
|
|
194
|
+
skillApiKey: "fch_test_key",
|
|
195
|
+
});
|
|
196
|
+
plugin.register(api);
|
|
197
|
+
|
|
198
|
+
const result = await tools.get("skill_publish")!.execute("call-5", {
|
|
199
|
+
filePath: "/tmp/strategy.zip",
|
|
200
|
+
visibility: "public",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const details = (result as { details: Record<string, unknown> }).details;
|
|
204
|
+
expect(details.success).toBe(true);
|
|
205
|
+
expect(details.slug).toBe("test-skill");
|
|
206
|
+
expect(details.submissionId).toBe("sub-new");
|
|
207
|
+
expect(readFile).toHaveBeenCalledWith("/tmp/strategy.zip");
|
|
208
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
209
|
+
expect(url).toContain("/api/v1/skill/publish");
|
|
210
|
+
expect(init.method).toBe("POST");
|
|
211
|
+
const body = JSON.parse(init.body as string);
|
|
212
|
+
expect(body.content).toBe("ZmFrZS16aXAtY29udGVudA==");
|
|
213
|
+
expect(body.visibility).toBe("public");
|
|
214
|
+
expect((init.headers as Record<string, string>)["Authorization"]).toBe("Bearer fch_test_key");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns error when submissionId and backtestTaskId both missing for verify", async () => {
|
|
218
|
+
const { api, tools } = createFakeApi({ skillApiKey: "fch_test" });
|
|
219
|
+
plugin.register(api);
|
|
220
|
+
|
|
221
|
+
const result = parseResult(await tools.get("skill_publish_verify")!.execute("call-6", {}));
|
|
222
|
+
|
|
223
|
+
expect(result.success).toBe(false);
|
|
224
|
+
expect(String(result.error)).toContain("Either submissionId or backtestTaskId is required");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("uses env SKILL_API_KEY when plugin config has no skillApiKey", async () => {
|
|
228
|
+
vi.stubEnv("SKILL_API_KEY", "fch_env_key");
|
|
229
|
+
|
|
230
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
231
|
+
ok: true,
|
|
232
|
+
status: 200,
|
|
233
|
+
text: async () => JSON.stringify({ submissionId: "sub-1", backtestStatus: "completed" }),
|
|
234
|
+
});
|
|
235
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
236
|
+
|
|
237
|
+
const { api, tools } = createFakeApi({ skillApiUrl: "http://skill.example" });
|
|
238
|
+
plugin.register(api);
|
|
239
|
+
|
|
240
|
+
await tools.get("skill_publish_verify")!.execute("call-7", { submissionId: "sub-1" });
|
|
241
|
+
|
|
242
|
+
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
243
|
+
expect((init.headers as Record<string, string>)["Authorization"]).toBe("Bearer fch_env_key");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("skill_validate", () => {
|
|
247
|
+
it("returns invalid when dirPath is missing", async () => {
|
|
248
|
+
const { api, tools } = createFakeApi({});
|
|
249
|
+
plugin.register(api);
|
|
250
|
+
const result = parseResult(await tools.get("skill_validate")!.execute("v1", { dirPath: "" }));
|
|
251
|
+
expect(result.valid).toBe(false);
|
|
252
|
+
expect(result.success).toBe(false);
|
|
253
|
+
expect(Array.isArray(result.errors) && result.errors.length > 0).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("returns invalid when fep.yaml is missing", async () => {
|
|
257
|
+
vi.mocked(readFile).mockRejectedValue(new Error("ENOENT"));
|
|
258
|
+
|
|
259
|
+
const { api, tools } = createFakeApi({});
|
|
260
|
+
plugin.register(api);
|
|
261
|
+
const result = parseResult(
|
|
262
|
+
await tools.get("skill_validate")!.execute("v2", { dirPath: "/tmp/empty" }),
|
|
263
|
+
);
|
|
264
|
+
expect(result.valid).toBe(false);
|
|
265
|
+
expect(result.errors).toBeDefined();
|
|
266
|
+
expect((result.errors as string[]).some((e: string) => e.includes("fep.yaml"))).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|