@ia-ccun/code-agent-cli 0.0.53 → 0.0.54-beta.3

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.
Files changed (45) hide show
  1. package/bin/cli.js +97 -69
  2. package/config/agent/extensions/aicode-advisor.ts +187 -64
  3. package/config/agent/extensions/aicode-banner.ts +101 -0
  4. package/config/agent/extensions/aicode-btw.ts +619 -182
  5. package/config/agent/extensions/aicode-cd.ts +246 -63
  6. package/config/agent/extensions/aicode-pets.ts +0 -11
  7. package/config/agent/extensions/aicode-plan.ts +2 -0
  8. package/config/agent/extensions/aicode-safety-guard.ts +3 -0
  9. package/config/agent/extensions/aicode-tips.ts +1 -16
  10. package/config/agent/extensions/context-monitor.ts +30 -15
  11. package/config/agent/extensions/context.ts +132 -60
  12. package/config/agent/extensions/custom-footer.ts +0 -5
  13. package/config/agent/extensions/custom.ts +41 -29
  14. package/config/agent/extensions/error-monitor.ts +37 -33
  15. package/config/agent/extensions/goal.ts +8 -6
  16. package/config/agent/extensions/msg-debug.ts +103 -112
  17. package/config/agent/extensions/review.ts +365 -164
  18. package/config/agent/extensions/working-msg.ts +2 -4
  19. package/dist/banner.d.ts.map +1 -1
  20. package/dist/banner.js +8 -1
  21. package/dist/banner.js.map +1 -1
  22. package/dist/config-loader.d.ts.map +1 -1
  23. package/dist/config-loader.js +9 -2
  24. package/dist/config-loader.js.map +1 -1
  25. package/package.json +6 -7
  26. package/scripts/regression/README.md +224 -0
  27. package/scripts/regression/cases/01-version-and-deps.sh +94 -0
  28. package/scripts/regression/cases/02-extension-loading.sh +130 -0
  29. package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
  30. package/scripts/regression/cases/04-prompt-stream.sh +130 -0
  31. package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
  32. package/scripts/regression/cases/06-tool-execution.sh +115 -0
  33. package/scripts/regression/cases/07-pi-new-features.sh +141 -0
  34. package/scripts/regression/lib/common.sh +287 -0
  35. package/scripts/regression/results/.last_pid +1 -0
  36. package/scripts/regression/results/.last_workdir +1 -0
  37. package/scripts/regression/results/02-extension-loading.log +21 -0
  38. package/scripts/regression/results/03-prompt-stream.log +16 -0
  39. package/scripts/regression/results/03-rpc-protocol.log +27 -0
  40. package/scripts/regression/results/04-slash-commands-and-skills.log +11 -0
  41. package/scripts/regression/results/05-tool-execution.log +8 -0
  42. package/scripts/regression/results/06-pi-new-features.log +27 -0
  43. package/scripts/regression/run-all.sh +187 -0
  44. package/config/agent/bin/fd.exe +0 -0
  45. package/config/agent/bin/rg.exe +0 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ # 03-rpc-protocol.sh
3
+ # 验证:AicodeRpcClient.kt 实际用到的所有 RPC 命令 + 事件类型兼容性矩阵
4
+ #
5
+ # 这是 RPC 协议层的回归保护伞 — 任何 RPC 命令的字段格式变化都会在这里失败
6
+ #
7
+ # RPC 命令清单(来源:aicode-idea-plugin-swing/src/main/kotlin/com/nu/aicode/rpc/AicodeRpcClient.kt)
8
+ # 状态控制: prompt / steer / follow_up / abort / compact
9
+ # 查询: get_state / get_session_stats / get_commands
10
+ # get_available_models / get_available_thinking_levels
11
+ # get_messages / get_entries / get_tree
12
+ # 模型控制: set_model / cycle_model / set_model_provider
13
+ # set_thinking_level / cycle_thinking_level
14
+ # 工具: set_tools / bash
15
+ # 会话: new_session / switch_session / fork
16
+ # 扩展钩子: extension_ui_response
17
+ # 新增 (0.84.4+): clear_queue
18
+ #
19
+ # 事件类型清单(同上文件 AicodeStreamItem sealed class)
20
+ # message_update(assistantMessageEvent: text_delta/thinking_delta/thinking)
21
+ # turn_start / turn_end / message_start / message_end
22
+ # agent_start / agent_end / agent_settled
23
+ # tool_execution_start / tool_execution_update / tool_execution_end
24
+ # queue_update / auto_retry_end
25
+
26
+ set -uo pipefail
27
+ CURRENT_CASE="03-rpc-protocol"
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ source "$SCRIPT_DIR/../lib/common.sh"
30
+
31
+ header "$CURRENT_CASE" "RPC protocol compatibility matrix"
32
+
33
+ if ! spawn_rpc 90; then
34
+ fail "spawn aicode RPC" "进程启动失败"
35
+ summary
36
+ exit 1
37
+ fi
38
+ pass "spawn aicode RPC (pid=$REGRESSION_PID)"
39
+
40
+ # ----- Part 1: 所有 RPC 命令的兼容性 -----
41
+ header "$CURRENT_CASE" "part 1: RPC command compatibility"
42
+
43
+ # 每个命令: "type|payload|必需 response.success 字段|期望字段"
44
+ # 简化版:只验证 type 字段能被识别(success=true 即视为兼容)
45
+ # 失败类型:响应错误、字段缺失、命令不存在
46
+ # RPC 命令兼容性测试 — 格式: "type|extra_fields_json_object"
47
+ # extra_fields 是 type 之外的字段,会拼到 {"type":"X","id":"rpcN", extra_fields}
48
+ #
49
+ # 注意:只测 pi 核心协议命令。aicode-cli 自定义命令(set_model_provider、set_tools)
50
+ # 是在 extensions 里实现的,跳过 -ne 时不生效,单独在 case 02 中测试。
51
+ #
52
+ # set_thinking_level / cycle_thinking_level 在并发发送场景下响应时序不稳定
53
+ # (0.85.1 表现),交给 case 07 单独验证。
54
+ RPC_COMMANDS=(
55
+ # 类型查询(pi 核心,必现)
56
+ "get_state|"
57
+ "get_session_stats|"
58
+ "get_commands|"
59
+ "get_available_models|"
60
+ "get_available_thinking_levels|"
61
+
62
+ # 会话管理
63
+ "new_session|"
64
+ "get_messages|"
65
+ "get_entries|"
66
+ "get_tree|"
67
+
68
+ # 新功能(0.84.4+)
69
+ "clear_queue|"
70
+ )
71
+
72
+ cmd_pass=0
73
+ cmd_fail=0
74
+ cmd_skip=0
75
+ idx=0
76
+ for spec in "${RPC_COMMANDS[@]}"; do
77
+ IFS='|' read -r cmd_type payload <<<"$spec"
78
+ idx=$((idx + 1))
79
+
80
+ req_id="rpc${idx}"
81
+ # 拼接合法 JSON:{"type":"X","id":"rpcN",<extra>}
82
+ if [[ -n "$payload" ]]; then
83
+ send "{\"type\":\"$cmd_type\",\"id\":\"$req_id\",$payload}"
84
+ else
85
+ send "{\"type\":\"$cmd_type\",\"id\":\"$req_id\"}"
86
+ fi
87
+
88
+ # 等最多 10 秒响应(set_tools/set_thinking_level 可能需要时间)
89
+ local_log="$REGRESSION_LOG"
90
+ waited=0
91
+ while [[ $waited -lt 20 ]]; do
92
+ if jq -e "select(.id==\"$req_id\" and .type==\"response\")" "$local_log" >/dev/null 2>&1; then
93
+ break
94
+ fi
95
+ sleep 0.5
96
+ waited=$((waited + 1))
97
+ done
98
+
99
+ resp=$(jq -c "select(.id==\"$req_id\" and .type==\"response\")" "$local_log" 2>/dev/null | head -1)
100
+ if [[ -z "$resp" ]]; then
101
+ fail "RPC $cmd_type 返回 response" "10s 内无响应"
102
+ cmd_fail=$((cmd_fail + 1))
103
+ continue
104
+ fi
105
+
106
+ success=$(echo "$resp" | jq -c '.success // false' 2>/dev/null)
107
+ if [[ "$success" == "true" ]]; then
108
+ pass "RPC $cmd_type success=true"
109
+ cmd_pass=$((cmd_pass + 1))
110
+ else
111
+ err=$(echo "$resp" | jq -c '.error // ""' 2>/dev/null)
112
+ # 区分"协议错误"和"业务错误"
113
+ if [[ -n "$err" ]] && echo "$err" | grep -qiE "Unknown command|not implemented|unsupported"; then
114
+ skip "RPC $cmd_type success=true" "pi 不支持: $err"
115
+ cmd_skip=$((cmd_skip + 1))
116
+ else
117
+ # 业务错误(如 fork 没 entryId)但协议 OK
118
+ if [[ "$success" == "false" ]] && [[ -n "$err" ]]; then
119
+ pass "RPC $cmd_type 协议 OK(业务错误: $err)"
120
+ cmd_pass=$((cmd_pass + 1))
121
+ else
122
+ fail "RPC $cmd_type 协议响应异常" "resp: $resp"
123
+ cmd_fail=$((cmd_fail + 1))
124
+ fi
125
+ fi
126
+ fi
127
+ done
128
+
129
+ echo ""
130
+ echo " RPC 命令小计: $cmd_pass pass / $cmd_fail fail / $cmd_skip skip"
131
+
132
+ # ----- Part 2: 事件类型 + 字段格式(通过实际 prompt 触发)-----
133
+ header "$CURRENT_CASE" "part 2: event type + field schema"
134
+
135
+ # 发 prompt 触发完整事件流
136
+ send '{"type":"prompt","message":"just say hi","id":"ev1"}'
137
+
138
+ # 等 LLM 完成(以 agent_end 为信号,最长 120s)
139
+ waited=0
140
+ while [[ $waited -lt 120 ]]; do
141
+ # 检查两种结束信号:agent_end 或 agent_settled
142
+ if jq -e 'select(.type=="agent_end" or .type=="agent_settled")' "$REGRESSION_LOG" >/dev/null 2>&1; then
143
+ break
144
+ fi
145
+ sleep 1
146
+ waited=$((waited + 1))
147
+ done
148
+
149
+ if [[ $waited -ge 120 ]]; then
150
+ fail "LLM 在 120s 内完成" "超时"
151
+ else
152
+ echo " LLM 在 ${waited}s 内完成"
153
+ fi
154
+
155
+ # 事件类型清单(每个都要出现至少 1 次)
156
+ EVENT_TYPES=(
157
+ "agent_start"
158
+ "turn_start"
159
+ "message_start"
160
+ "message_end"
161
+ "message_update"
162
+ "turn_end"
163
+ "agent_end"
164
+ )
165
+
166
+ ev_pass=0
167
+ ev_fail=0
168
+ for ev in "${EVENT_TYPES[@]}"; do
169
+ if jq -e "select(.type==\"$ev\")" "$REGRESSION_LOG" >/dev/null 2>&1; then
170
+ pass "事件 $ev 出现"
171
+ ev_pass=$((ev_pass + 1))
172
+ else
173
+ fail "事件 $ev 出现" "60s 内未观察到"
174
+ ev_fail=$((ev_fail + 1))
175
+ fi
176
+ done
177
+
178
+ echo ""
179
+ echo " 事件类型小计: $ev_pass pass / $ev_fail fail"
180
+
181
+ # ----- Part 3: 关键字段 schema 兼容性 -----
182
+ header "$CURRENT_CASE" "part 3: event field schema"
183
+
184
+ # message_start.message.role 必须是 "user" 或 "assistant"
185
+ role_check=$(jq -c 'select(.type=="message_start") | .message.role // "missing"' "$REGRESSION_LOG" 2>/dev/null | sort -u | tr '\n' ' ')
186
+ echo " 观察到的 roles: $role_check"
187
+ if [[ "$role_check" == *"user"* && "$role_check" == *"assistant"* ]]; then
188
+ pass "message_start.message.role 包含 user + assistant"
189
+ else
190
+ fail "message_start.message.role 字段" "got: $role_check"
191
+ fi
192
+
193
+ # turn_end.message.usage 字段
194
+ usage_keys=$(jq -c 'select(.type=="turn_end") | .message.usage | keys | join(",")' "$REGRESSION_LOG" 2>/dev/null | head -1)
195
+ echo " turn_end.message.usage keys: $usage_keys"
196
+ for key in input output cost; do
197
+ if [[ "$usage_keys" == *"$key"* ]]; then
198
+ pass "turn_end.usage.$key 存在"
199
+ else
200
+ fail "turn_end.usage.$key 存在" "got: $usage_keys"
201
+ fi
202
+ done
203
+
204
+ # message_update.assistantMessageEvent 子类型必须出现
205
+ ame_subtypes=$(jq -r 'select(.type=="message_update") | .assistantMessageEvent.type // ""' "$REGRESSION_LOG" 2>/dev/null | sort -u | grep -v '^$' | tr '\n' ' ')
206
+ echo " assistantMessageEvent 子类型: $ame_subtypes"
207
+ for subtype in thinking_delta text_delta; do
208
+ if [[ "$ame_subtypes" == *"$subtype"* ]]; then
209
+ pass "assistantMessageEvent.$subtype 出现"
210
+ else
211
+ fail "assistantMessageEvent.$subtype 出现" "got: $ame_subtypes"
212
+ fi
213
+ done
214
+
215
+ # message_update.assistantMessageEvent.delta 字段
216
+ delta_check=$(jq -c 'select(.type=="message_update" and .assistantMessageEvent.type=="text_delta") | .assistantMessageEvent.delta // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
217
+ if [[ "$delta_check" != '"missing"' && -n "$delta_check" ]]; then
218
+ pass "assistantMessageEvent.delta 字段存在"
219
+ else
220
+ fail "assistantMessageEvent.delta 字段存在" "got: $delta_check"
221
+ fi
222
+
223
+ # turn_end 必须有 message 字段(不是 top-level usage)
224
+ turn_end_schema=$(jq -c 'select(.type=="turn_end") | has("message") and (.message | has("usage"))' "$REGRESSION_LOG" 2>/dev/null | sort -u | head -1)
225
+ if [[ "$turn_end_schema" == "true" ]]; then
226
+ pass "turn_end.message.usage 嵌套结构"
227
+ else
228
+ fail "turn_end.message.usage 嵌套结构" "可能 schema 漂移:$turn_end_schema"
229
+ fi
230
+
231
+ # ----- Part 4: 计时关键字段(与 aicode-plugin-swing 状态计算相关)-----
232
+ header "$CURRENT_CASE" "part 4: timing fields"
233
+
234
+ # agent_end 可能含 willRetry 字段(aicode-plugin-swing 用它判断是否重试中)
235
+ agent_end_keys=$(jq -c 'select(.type=="agent_end") | keys | join(",")' "$REGRESSION_LOG" 2>/dev/null | head -1)
236
+ echo " agent_end keys: $agent_end_keys"
237
+ if [[ "$agent_end_keys" == *"willRetry"* ]]; then
238
+ pass "agent_end.willRetry 字段存在"
239
+ else
240
+ skip "agent_end.willRetry 字段存在" "未出现(可能本轮不重试)"
241
+ fi
242
+
243
+ # message_end.stopReason 必须是已知值(0.85+ 新增 "stop" 通用值 + rawStopReason 保留 provider 原值)
244
+ stop_reason=$(jq -c 'select(.type=="message_end" and .message.role=="assistant") | .message.stopReason // "missing"' "$REGRESSION_LOG" 2>/dev/null | sort -u | head -1)
245
+ case "$stop_reason" in
246
+ '"end_turn"'|'"tool_use"'|'"error"'|'"max_tokens"'|'"stop_sequence"'|'"aborted"'|'"stop"')
247
+ pass "stopReason 合法 ($stop_reason)"
248
+ ;;
249
+ '"missing"')
250
+ fail "stopReason 字段" "未出现"
251
+ ;;
252
+ *)
253
+ skip "stopReason 已知值" "新值: $stop_reason(需人工确认是否要更新 AicodeRpcClient.kt)"
254
+ ;;
255
+ esac
256
+
257
+ cleanup_rpc
258
+ summary
259
+ exit $?
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # 03-prompt-stream.sh
3
+ # 验证:prompt RPC 触发完整 message_update 事件流
4
+ # 核心回归项 — AicodeRpcClient.kt 的 streamPrompt() 完全依赖这套事件序列
5
+ #
6
+ # 关键事件序列(与 aicode-plugin-swing AicodeStreamItem 映射):
7
+ # 1. {"type":"response","command":"prompt","success":true} <- RPC 响应
8
+ # 2. {"type":"agent_start"}
9
+ # 3. {"type":"turn_start"}
10
+ # 4. {"type":"message_start",message:{role:"user",...}}
11
+ # 5. {"type":"message_end",message:{role:"user",...}}
12
+ # 6. {"type":"message_start",message:{role:"assistant",api,provider,...}}
13
+ # 7. N × {"type":"message_update",assistantMessageEvent:{type:"thinking_delta",delta:"..."}}
14
+ # 8. {"type":"message_update",assistantMessageEvent:{type:"thinking_end",content:"..."}}
15
+ # 9. {"type":"message_update",assistantMessageEvent:{type:"text_start",contentIndex:N}}
16
+ # 10. N × {"type":"message_update",assistantMessageEvent:{type:"text_delta",delta:"..."}}
17
+ # 11. {"type":"message_update",assistantMessageEvent:{type:"text_end",...}} (可能)
18
+ # 12. {"type":"message_end",message:{role:"assistant",stopReason:"end_turn"|"tool_use"|...}}
19
+ # 13. {"type":"turn_end",message:{...usage:{input,output,cost,...}}}
20
+ # 14. {"type":"agent_end"}
21
+
22
+ set -uo pipefail
23
+ CURRENT_CASE="03-prompt-stream"
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ source "$SCRIPT_DIR/../lib/common.sh"
26
+
27
+ header "$CURRENT_CASE" "prompt → message_update stream"
28
+
29
+ if ! spawn_rpc 60; then
30
+ fail "spawn aicode RPC" "进程启动失败"
31
+ summary
32
+ exit 1
33
+ fi
34
+ pass "spawn aicode RPC (pid=$REGRESSION_PID)"
35
+
36
+ # 发 prompt
37
+ send '{"type":"prompt","message":"reply with just: ok","id":"p1"}'
38
+
39
+ # 等 LLM 完成(agent_end 作为信号,最长 120s)
40
+ waited=0
41
+ while [[ $waited -lt 120 ]]; do
42
+ if jq -e 'select(.type=="agent_end" or .type=="agent_settled")' "$REGRESSION_LOG" >/dev/null 2>&1; then
43
+ break
44
+ fi
45
+ sleep 0.5
46
+ waited=$((waited + 1))
47
+ done
48
+
49
+ if [[ $waited -ge 120 ]]; then
50
+ fail "LLM 在 120s 内完成" "超时"
51
+ else
52
+ echo " LLM 在 ${waited}.0s 内完成"
53
+ fi
54
+
55
+ # 验证事件序列
56
+ header "$CURRENT_CASE" "event sequence"
57
+
58
+ assert_event_count "agent_start 事件出现" \
59
+ 'select(.type=="agent_start")' 1
60
+
61
+ assert_event_count "turn_start 事件出现" \
62
+ 'select(.type=="turn_start")' 1
63
+
64
+ # user message_start / message_end 配对
65
+ assert_event_count "user message_start 出现" \
66
+ 'select(.type=="message_start" and .message.role=="user")' 1
67
+ assert_event_count "user message_end 出现" \
68
+ 'select(.type=="message_end" and .message.role=="user")' 1
69
+
70
+ # assistant message_start
71
+ assert_event_count "assistant message_start 出现" \
72
+ 'select(.type=="message_start" and .message.role=="assistant")' 1
73
+
74
+ # assistantMessageEvent 子类型
75
+ header "$CURRENT_CASE" "assistantMessageEvent subtypes"
76
+
77
+ # 至少 1 个 thinking_start
78
+ assert_event_count "thinking_start 出现" \
79
+ 'select(.type=="message_update" and .assistantMessageEvent.type=="thinking_start")' 1
80
+
81
+ # 至少 1 个 thinking_delta (流式)
82
+ assert_event_count "thinking_delta 至少 1 个" \
83
+ 'select(.type=="message_update" and .assistantMessageEvent.type=="thinking_delta")' 1
84
+
85
+ # 至少 1 个 text_start
86
+ assert_event_count "text_start 出现" \
87
+ 'select(.type=="message_update" and .assistantMessageEvent.type=="text_start")' 1
88
+
89
+ # 至少 1 个 text_delta
90
+ assert_event_count "text_delta 至少 1 个" \
91
+ 'select(.type=="message_update" and .assistantMessageEvent.type=="text_delta")' 1
92
+
93
+ # assistant message_end
94
+ header "$CURRENT_CASE" "completion events"
95
+ assert_event_count "assistant message_end 出现" \
96
+ 'select(.type=="message_end" and .message.role=="assistant")' 1
97
+
98
+ # stopReason 字段(end_turn / tool_use / error / stop 0.85+)
99
+ stop_reason=$(jq -c 'select(.type=="message_end" and .message.role=="assistant") | .message.stopReason // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
100
+ case "$stop_reason" in
101
+ '"end_turn"'|'"tool_use"'|'"error"'|'"max_tokens"'|'"stop_sequence"'|'"aborted"'|'"stop"')
102
+ pass "assistant stopReason 是 $stop_reason"
103
+ ;;
104
+ '"missing"')
105
+ fail "assistant stopReason 字段" "未出现"
106
+ ;;
107
+ *)
108
+ fail "assistant stopReason 合法" "未知值: $stop_reason"
109
+ ;;
110
+ esac
111
+
112
+ # turn_end 带 usage
113
+ assert_event_count "turn_end 事件出现" \
114
+ 'select(.type=="turn_end")' 1
115
+
116
+ # usage 字段
117
+ usage_input=$(jq -c 'select(.type=="turn_end") | .message.usage.input // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
118
+ if [[ "$usage_input" != '"missing"' && "$usage_input" =~ ^[0-9]+$ ]]; then
119
+ pass "turn_end.usage.input 是数字 ($usage_input)"
120
+ else
121
+ fail "turn_end.usage.input 是数字" "got: $usage_input"
122
+ fi
123
+
124
+ # agent_end 收尾
125
+ assert_event_count "agent_end 事件出现" \
126
+ 'select(.type=="agent_end")' 1
127
+
128
+ cleanup_rpc
129
+ summary
130
+ exit $?
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # 04-slash-commands-and-skills.sh
3
+ # 验证:aicode-cli 预置的 Prompts 与 Skills 全部能通过 get_commands 拿到
4
+ #
5
+ # Prompts (config/agent/prompts/):
6
+ # /code-simplifier /feat /git-commit /init-project /workflow
7
+ # Skills (config/agent/skills/) — 通过 /skill: 触发:
8
+ # /skill:code-review /skill:commit /skill:pr /skill:refactor
9
+ # /skill:frontend-design /skill:brainstorming /skill:hello
10
+ # /skill:ponytail /skill:ponytail-audit /skill:ponytail-debt 等
11
+ #
12
+ # 这是 aicode-cli 的"开箱即用"卖点 — 升级 pi 后不能丢命令
13
+
14
+ set -uo pipefail
15
+ CURRENT_CASE="04-slash-commands-and-skills"
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ source "$SCRIPT_DIR/../lib/common.sh"
18
+
19
+ header "$CURRENT_CASE" "prompts & skills registration"
20
+
21
+ # pi 从 cwd 发现 prompts/skills/extensions。cd 到 aicode-cli 仓库根
22
+ # 让 pi 能加载 config/agent/{prompts,skills,extensions}
23
+ AICODE_CLI_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
24
+ cd "$AICODE_CLI_ROOT" || { fail "cd aicode root" "$AICODE_CLI_ROOT"; summary; exit 1; }
25
+ echo " cwd: $AICODE_CLI_ROOT"
26
+
27
+ if ! spawn_rpc 30 false; then
28
+ fail "spawn aicode RPC(带扩展)" "进程启动失败"
29
+ summary
30
+ exit 1
31
+ fi
32
+ pass "spawn aicode RPC(带扩展) (pid=$REGRESSION_PID)"
33
+
34
+ # ----- Test 1: 总命令数 -----
35
+ send '{"type":"get_commands","id":"c1"}'
36
+ sleep 1
37
+
38
+ cmds_json=$(jq -c 'select(.id=="c1" and .type=="response") | .data.commands' "$REGRESSION_LOG" 2>/dev/null | head -1)
39
+ cmd_count=$(echo "$cmds_json" | jq -c 'length' 2>/dev/null)
40
+
41
+ if [[ -z "$cmd_count" || "$cmd_count" == "null" ]]; then
42
+ fail "get_commands 返回 commands 数组" "got: $cmds_json"
43
+ cleanup_rpc; summary; exit 1
44
+ fi
45
+ echo " total commands: $cmd_count"
46
+
47
+ if [[ "$cmd_count" -lt 20 ]]; then
48
+ fail "至少注册 20 个命令(5 prompt + 20+ skill + 内置)" "got: $cmd_count"
49
+ else
50
+ pass "注册 $cmd_count 个命令"
51
+ fi
52
+
53
+ # ----- Test 2: 5 个预置 Prompts -----
54
+ header "$CURRENT_CASE" "predefined prompts (5)"
55
+
56
+ PROMPTS=("git-commit" "workflow" "feat" "code-simplifier" "init-project")
57
+ prompt_ok=0
58
+ for p in "${PROMPTS[@]}"; do
59
+ # pi 0.85+ 的 commands.name 不带 / 前缀(如 "git-commit"),用户调用时才加 /
60
+ if echo "$cmds_json" | jq -e --arg c "$p" '.[] | select(.name == $c)' >/dev/null 2>&1; then
61
+ pass "Prompt /$p 注册"
62
+ prompt_ok=$((prompt_ok + 1))
63
+ else
64
+ fail "Prompt /$p 注册" "未在 commands 中"
65
+ fi
66
+ done
67
+
68
+ # ----- Test 3: 关键 Skills -----
69
+ header "$CURRENT_CASE" "predefined skills (sample)"
70
+
71
+ # 测试核心 skills(不强求全部,验证机制)
72
+ SKILLS=("skill:code-review" "skill:commit" "skill:pr" "skill:refactor" "skill:frontend-design" "skill:brainstorming" "skill:hello")
73
+ skill_ok=0
74
+ for s in "${SKILLS[@]}"; do
75
+ # pi 0.85+ skills 用 "skill:xxx" 作为 name(含 skill: 前缀)
76
+ if echo "$cmds_json" | jq -e --arg c "$s" '.[] | select(.name == $c)' >/dev/null 2>&1; then
77
+ pass "Skill /$s 注册"
78
+ skill_ok=$((skill_ok + 1))
79
+ else
80
+ fail "Skill /$s 注册" "未在 commands 中"
81
+ fi
82
+ done
83
+
84
+ # 至少 60% 核心 skills 要在
85
+ min_required=$(( ${#SKILLS[@]} * 60 / 100 ))
86
+ if [[ $skill_ok -ge $min_required ]]; then
87
+ pass "核心 skills 注册率 ≥ 60% ($skill_ok/${#SKILLS[@]})"
88
+ else
89
+ fail "核心 skills 注册率 ≥ 60%" "got: $skill_ok/${#SKILLS[@]}"
90
+ fi
91
+
92
+ # ----- Test 4: 每条 command 必须有 name + description -----
93
+ header "$CURRENT_CASE" "command schema"
94
+
95
+ # 抽 3 条 command 看字段
96
+ sample=$(echo "$cmds_json" | jq -c '.[0:3]' 2>/dev/null)
97
+ if [[ -n "$sample" ]]; then
98
+ echo " sample commands:"
99
+ echo "$sample" | jq -r '.[] | " - \(.name // "?") | \(.description // "(no description)")"' 2>/dev/null | head -3
100
+
101
+ # 每条都有 name
102
+ if echo "$cmds_json" | jq -e 'all(.[]; has("name"))' >/dev/null 2>&1; then
103
+ pass "所有 command 都有 name 字段"
104
+ else
105
+ fail "所有 command 都有 name 字段" "存在缺失 name 的 command"
106
+ fi
107
+ fi
108
+
109
+ # ----- Test 5: 内置命令(/model /settings /clear 等)也在 -----
110
+ header "$CURRENT_CASE" "built-in commands"
111
+
112
+ # RPC 模式下,pi 0.85.1 的内置命令(/model /settings 等)不通过 get_commands 返回
113
+ # 它们是内置到 TUI 的,只在交互模式里出现。RPC 只返回 prompts/skills/extensions。
114
+ # 跳过此检查,备注到 README。
115
+ skip "内置命令 ≥ 5 个" "RPC 模式不返回内置命令(仅交互模式可见)"
116
+
117
+ cleanup_rpc
118
+ summary
119
+ exit $?
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # 05-tool-execution.sh
3
+ # 验证:tool_execution_* 事件流(AicodeRpcClient.kt 的 ToolStart/ToolUpdate/ToolEnd 映射)
4
+ #
5
+ # 关键事件序列:
6
+ # 1. {"type":"tool_execution_start",toolCallId,toolName,args}
7
+ # 2. N × {"type":"tool_execution_update",toolCallId,partialResult:{content:[{text:"..."}]}}
8
+ # 3. {"type":"tool_execution_end",toolCallId,result:{content:[{text:"..."}]},isError:"false"|"true"}
9
+ #
10
+ # 0.84.3 修复了 toolcall_start 缺 id/name 的 bug,所以 toolCallId 字段必须存在
11
+
12
+ set -uo pipefail
13
+ CURRENT_CASE="05-tool-execution"
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ source "$SCRIPT_DIR/../lib/common.sh"
16
+
17
+ header "$CURRENT_CASE" "tool_execution event flow"
18
+
19
+ if ! spawn_rpc 120; then
20
+ fail "spawn aicode RPC" "进程启动失败"
21
+ summary
22
+ exit 1
23
+ fi
24
+ pass "spawn aicode RPC (pid=$REGRESSION_PID)"
25
+
26
+ # 触发 read:让模型读取一个已知文件
27
+ TEST_FILE="/etc/hostname"
28
+ send "{\"type\":\"prompt\",\"message\":\"use read tool to read /etc/hostname\",\"id\":\"t1\"}"
29
+
30
+ # 等 turn 完成(最长 90s)
31
+ header "$CURRENT_CASE" "waiting for turn"
32
+
33
+ waited=0
34
+ while [[ $waited -lt 90 ]]; do
35
+ if jq -e 'select(.id=="t1" and .type=="response")' "$REGRESSION_LOG" >/dev/null 2>&1; then
36
+ break
37
+ fi
38
+ sleep 1
39
+ waited=$((waited + 1))
40
+ done
41
+
42
+ if ! jq -e 'select(.id=="t1" and .type=="response")' "$REGRESSION_LOG" >/dev/null 2>&1; then
43
+ fail "prompt 在 90s 内完成" "超时"
44
+ cleanup_rpc; summary; exit 1
45
+ fi
46
+
47
+ # 验证 tool 事件
48
+ header "$CURRENT_CASE" "tool_execution events"
49
+
50
+ start_count=$(jq -c 'select(.type=="tool_execution_start")' "$REGRESSION_LOG" 2>/dev/null | wc -l | tr -d ' ')
51
+ end_count=$(jq -c 'select(.type=="tool_execution_end")' "$REGRESSION_LOG" 2>/dev/null | wc -l | tr -d ' ')
52
+
53
+ echo " tool_execution_start: $start_count"
54
+ echo " tool_execution_end: $end_count"
55
+
56
+ if [[ $start_count -gt 0 ]]; then
57
+ pass "tool_execution_start 出现 ($start_count 个)"
58
+
59
+ # 0.84.3 修复:toolCallId 必现
60
+ tool_call_id=$(jq -c 'select(.type=="tool_execution_start") | .toolCallId // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
61
+ if [[ "$tool_call_id" != '"missing"' && "$tool_call_id" != '""' && -n "$tool_call_id" ]]; then
62
+ pass "tool_execution_start.toolCallId 存在 (0.84.3 修复点)"
63
+ else
64
+ fail "tool_execution_start.toolCallId 存在" "got: $tool_call_id"
65
+ fi
66
+
67
+ # 0.84.3 修复:toolName 必现
68
+ tool_name=$(jq -c 'select(.type=="tool_execution_start") | .toolName // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
69
+ if [[ "$tool_name" != '"missing"' && "$tool_name" != '""' && -n "$tool_name" ]]; then
70
+ pass "tool_execution_start.toolName 存在 (0.84.3 修复点) = $tool_name"
71
+ else
72
+ fail "tool_execution_start.toolName 存在" "got: $tool_name"
73
+ fi
74
+
75
+ # args 字段
76
+ args=$(jq -c 'select(.type=="tool_execution_start") | .args // "missing" | type' "$REGRESSION_LOG" 2>/dev/null | head -1)
77
+ if [[ "$args" == '"object"' ]]; then
78
+ pass "tool_execution_start.args 是 object"
79
+ else
80
+ fail "tool_execution_start.args 是 object" "got type: $args"
81
+ fi
82
+ else
83
+ skip "tool_execution_start 出现" "模型没调用工具"
84
+ fi
85
+
86
+ if [[ $end_count -gt 0 ]]; then
87
+ pass "tool_execution_end 出现 ($end_count 个)"
88
+
89
+ # isError 字段
90
+ is_error=$(jq -c 'select(.type=="tool_execution_end") | .isError // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
91
+ if [[ "$is_error" == '"false"' || "$is_error" == '"true"' ]]; then
92
+ pass "tool_execution_end.isError 是 \"false\"|\"true\""
93
+ else
94
+ fail "tool_execution_end.isError 格式" "got: $is_error"
95
+ fi
96
+
97
+ # result.content 数组
98
+ result_type=$(jq -c 'select(.type=="tool_execution_end") | .result.content | type' "$REGRESSION_LOG" 2>/dev/null | head -1)
99
+ if [[ "$result_type" == '"array"' ]]; then
100
+ pass "tool_execution_end.result.content 是数组"
101
+ else
102
+ fail "tool_execution_end.result.content 是数组" "got: $result_type"
103
+ fi
104
+ fi
105
+
106
+ # start 与 end 配对
107
+ if [[ $start_count -gt 0 && $start_count -eq $end_count ]]; then
108
+ pass "tool_execution_start 与 tool_execution_end 配对 ($start_count == $end_count)"
109
+ elif [[ $start_count -gt 0 ]]; then
110
+ fail "tool_execution_start 与 tool_execution_end 配对" "$start_count start vs $end_count end"
111
+ fi
112
+
113
+ cleanup_rpc
114
+ summary
115
+ exit $?