@ia-ccun/code-agent-cli 0.0.52 → 0.0.53-beta.1

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 (75) hide show
  1. package/bin/cli.js +6 -2
  2. package/config/agent/bin/README.md +102 -0
  3. package/config/agent/extensions/aicode-advisor.ts +13 -0
  4. package/config/agent/extensions/aicode-btw.ts +66 -14
  5. package/config/agent/extensions/aicode-cd.ts +834 -0
  6. package/config/agent/extensions/aicode-grillMe.ts +198 -0
  7. package/config/agent/extensions/aicode-pets.ts +362 -0
  8. package/config/agent/extensions/aicode-safety-guard.ts +552 -0
  9. package/config/agent/extensions/aicode-tips.ts +70 -16
  10. package/config/agent/extensions/aicode-todo-progress.ts +547 -0
  11. package/config/agent/extensions/custom.ts +93 -95
  12. package/config/agent/extensions/error-monitor.ts +1 -18
  13. package/config/agent/extensions/ponytail.ts +441 -0
  14. package/config/agent/extensions/tps.ts +87 -0
  15. package/config/agent/extensions/working-msg.ts +165 -50
  16. package/config/agent/help.md +0 -7
  17. package/config/agent/models.json +7 -7
  18. package/config/agent/settings.json +2 -1
  19. package/config/agent/skills/chroma/LICENSE +21 -0
  20. package/config/agent/skills/chroma/chroma-local/SKILL.md +165 -0
  21. package/config/agent/skills/chroma/chroma-local/cli.md +48 -0
  22. package/config/agent/skills/chroma/chroma-local/data-model.md +103 -0
  23. package/config/agent/skills/chroma/chroma-local/error-handling/python.md +166 -0
  24. package/config/agent/skills/chroma/chroma-local/error-handling/typescript.md +231 -0
  25. package/config/agent/skills/chroma/chroma-local/local-chroma/python.md +95 -0
  26. package/config/agent/skills/chroma/chroma-local/local-chroma/typescript.md +101 -0
  27. package/config/agent/skills/chroma/chroma-local/metadata/python.md +154 -0
  28. package/config/agent/skills/chroma/chroma-local/metadata/typescript.md +153 -0
  29. package/config/agent/skills/chroma/chroma-local/querying/python.md +147 -0
  30. package/config/agent/skills/chroma/chroma-local/querying/typescript.md +157 -0
  31. package/config/agent/skills/chroma/chroma-local/regex/python.md +69 -0
  32. package/config/agent/skills/chroma/chroma-local/regex/typescript.md +71 -0
  33. package/config/agent/skills/chroma/chroma-local/understanding-a-codebase.md +80 -0
  34. package/config/agent/skills/chroma/chroma-local/updating-deleting/python.md +159 -0
  35. package/config/agent/skills/chroma/chroma-local/updating-deleting/typescript.md +228 -0
  36. package/config/agent/skills/milvus-skill/LICENSE +202 -0
  37. package/config/agent/skills/milvus-skill/README.md +37 -0
  38. package/config/agent/skills/milvus-skill/SKILL.md +259 -0
  39. package/config/agent/skills/milvus-skill/references/collection.md +86 -0
  40. package/config/agent/skills/milvus-skill/references/database.md +24 -0
  41. package/config/agent/skills/milvus-skill/references/index.md +69 -0
  42. package/config/agent/skills/milvus-skill/references/partition.md +29 -0
  43. package/config/agent/skills/milvus-skill/references/patterns.md +150 -0
  44. package/config/agent/skills/milvus-skill/references/user-role.md +90 -0
  45. package/config/agent/skills/milvus-skill/references/vector.md +247 -0
  46. package/config/agent/skills/ponytail/SKILL.md +100 -0
  47. package/config/agent/skills/ponytail-audit/SKILL.md +40 -0
  48. package/config/agent/skills/ponytail-debt/SKILL.md +44 -0
  49. package/config/agent/skills/ponytail-help/SKILL.md +68 -0
  50. package/config/agent/skills/ponytail-review/SKILL.md +56 -0
  51. package/config/agent/skills/zread/README.md +115 -0
  52. package/config/agent/skills/zread/SKILL.md +103 -0
  53. package/config/agent/skills/zread/references/stdio-protocol.md +312 -0
  54. package/package.json +7 -6
  55. package/scripts/postinstall.js +58 -1
  56. package/scripts/regression/README.md +224 -0
  57. package/scripts/regression/cases/01-version-and-deps.sh +94 -0
  58. package/scripts/regression/cases/02-extension-loading.sh +130 -0
  59. package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
  60. package/scripts/regression/cases/04-prompt-stream.sh +130 -0
  61. package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
  62. package/scripts/regression/cases/06-tool-execution.sh +115 -0
  63. package/scripts/regression/cases/07-pi-new-features.sh +141 -0
  64. package/scripts/regression/lib/common.sh +287 -0
  65. package/scripts/regression/results/.last_pid +1 -0
  66. package/scripts/regression/results/.last_workdir +1 -0
  67. package/scripts/regression/results/02-extension-loading.log +29 -0
  68. package/scripts/regression/results/03-prompt-stream.log +25 -0
  69. package/scripts/regression/results/03-rpc-protocol.log +38 -0
  70. package/scripts/regression/results/04-slash-commands-and-skills.log +10 -0
  71. package/scripts/regression/results/05-tool-execution.log +6 -0
  72. package/scripts/regression/results/06-pi-new-features.log +33 -0
  73. package/scripts/regression/run-all.sh +187 -0
  74. package/config/agent/dangerous-patterns.json +0 -106
  75. package/config/agent/extensions/bash-check.ts +0 -227
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # 01-version-and-deps.sh
3
+ # 验证:aicode 二进制版本 + 内嵌 pi 包版本
4
+ # 这是回归测试的入口,确认我们用的版本符合预期
5
+
6
+ set -uo pipefail
7
+ CURRENT_CASE="01-version-and-deps"
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$SCRIPT_DIR/../lib/common.sh"
10
+
11
+ header "$CURRENT_CASE" "version & pi dependencies"
12
+
13
+ # ----- Test 1: aicode -v 输出 -----
14
+ header "$CURRENT_CASE" "aicode binary"
15
+ if ! command -v "$AICODE_BIN" >/dev/null 2>&1; then
16
+ fail "aicode 二进制存在" "未找到 $AICODE_BIN,设 AICODE_BIN 环境变量"
17
+ summary
18
+ exit 1
19
+ fi
20
+ pass "aicode 二进制存在 ($AICODE_BIN)"
21
+
22
+ VERSION=$("$AICODE_BIN" -v 2>&1 | head -1)
23
+ echo " version: $VERSION"
24
+
25
+ if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$ ]]; then
26
+ pass "版本号格式合规 ($VERSION)"
27
+ else
28
+ fail "版本号格式合规" "got: $VERSION"
29
+ fi
30
+
31
+ # ----- Test 2: 内嵌 pi 包版本 -----
32
+ header "$CURRENT_CASE" "pi package dependencies"
33
+
34
+ # 找到 aicode 安装路径里的 package.json
35
+ AICODE_PATH=$(command -v "$AICODE_BIN")
36
+ # 解析 symlink
37
+ if [[ -L "$AICODE_PATH" ]]; then
38
+ AICODE_PATH=$(readlink "$AICODE_PATH")
39
+ fi
40
+
41
+ # 找 package.json
42
+ RESOLVED_PATH=$(node -e "
43
+ const path = require('path');
44
+ const fs = require('fs');
45
+ let p = '$AICODE_PATH';
46
+ // aicode -> ../lib/node_modules/@ia-ccun/code-agent-cli/bin/cli.js
47
+ if (p.includes('/lib/node_modules/')) {
48
+ p = p.split('/lib/node_modules/')[0] + '/lib/node_modules/@ia-ccun/code-agent-cli';
49
+ } else if (p.includes('/.bin/')) {
50
+ p = path.join(path.dirname(p), '..', 'lib', 'node_modules', '@ia-ccun', 'code-agent-cli');
51
+ }
52
+ try {
53
+ const pkg = JSON.parse(fs.readFileSync(path.join(p, 'package.json'), 'utf8'));
54
+ console.log(p);
55
+ console.log(JSON.stringify(pkg.dependencies || {}));
56
+ } catch (e) { console.error(e.message); process.exit(1); }
57
+ " 2>&1) || {
58
+ fail "解析 aicode 安装路径" "node 解析失败"
59
+ summary
60
+ exit 1
61
+ }
62
+
63
+ AICODE_ROOT=$(echo "$RESOLVED_PATH" | head -1)
64
+ DEPS_JSON=$(echo "$RESOLVED_PATH" | tail -1)
65
+ echo " install path: $AICODE_ROOT"
66
+ echo " declared deps: $DEPS_JSON"
67
+
68
+ # 检查 4 个核心 pi 包
69
+ for pkg in @earendil-works/pi-agent-core @earendil-works/pi-ai @earendil-works/pi-coding-agent @earendil-works/pi-tui; do
70
+ declared=$(echo "$DEPS_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$pkg','MISSING'))" 2>/dev/null)
71
+ if [[ "$declared" == "MISSING" || -z "$declared" ]]; then
72
+ fail "$pkg 已在 dependencies 声明" "declared: $declared"
73
+ continue
74
+ fi
75
+ pass "$pkg declared as $declared"
76
+
77
+ # 检查实际安装版本
78
+ actual=$("$AICODE_ROOT/node_modules/$pkg/package.json" -e "console.log(require('$AICODE_ROOT/node_modules/$pkg/package.json').version)" 2>/dev/null)
79
+ if [[ -n "$actual" ]]; then
80
+ # 解析 semver 主要版本
81
+ declared_major=$(echo "$declared" | sed -E 's/^[^0-9]*([0-9]+).*/\1/')
82
+ actual_major=$(echo "$actual" | sed -E 's/^[^0-9]*([0-9]+).*/\1/')
83
+ if [[ "$declared_major" == "$actual_major" ]]; then
84
+ pass "$pkg 安装版本 $actual 满足 ^$declared_major"
85
+ else
86
+ fail "$pkg 主版本一致" "declared major=$declared_major | actual=$actual"
87
+ fi
88
+ else
89
+ fail "$pkg 实际安装版本" "无法读取 $AICODE_ROOT/node_modules/$pkg/package.json"
90
+ fi
91
+ done
92
+
93
+ summary
94
+ exit $?
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # 02-extension-loading.sh
3
+ # 验证:aicode-cli 自带的 20 个 TypeScript 扩展在 RPC 启动时全部成功加载
4
+ #
5
+ # 这是 aicode-cli 的核心差异化能力:
6
+ # - aicode-btw / aicode-plan / aicode-advisor / aicode-cd / aicode-grillMe
7
+ # - aicode-safety-guard / aicode-todo-progress / aicode-tips / aicode-pets
8
+ # - context-monitor / context / error-monitor / working-msg / tps
9
+ # - goal / review / ponytail / custom / custom-footer / msg-debug
10
+ #
11
+ # 升级 pi 后扩展 API 可能微调,必须验证:
12
+ # 1. 启动时没有任何 "Failed to load extension" 错误
13
+ # 2. 没有任何工具名冲突(conflict error)
14
+ # 3. 至少 N 个 aicode-* 扩展在 .agents.md 或 get_commands 中可见
15
+
16
+ set -uo pipefail
17
+ CURRENT_CASE="02-extension-loading"
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ source "$SCRIPT_DIR/../lib/common.sh"
20
+
21
+ header "$CURRENT_CASE" "aicode custom extensions"
22
+
23
+ if ! spawn_rpc; then
24
+ fail "spawn aicode RPC(带扩展)" "进程启动失败"
25
+ summary
26
+ exit 1
27
+ fi
28
+ pass "spawn aicode RPC (pid=$REGRESSION_PID)"
29
+
30
+ # ----- Test 1: 启动日志中无扩展加载失败 -----
31
+ header "$CURRENT_CASE" "extension load errors"
32
+
33
+ # 把 stderr/stdout 全捕获到 log(-ne 关闭扩展 → 我们用普通模式)
34
+ cleanup_rpc
35
+
36
+ # 重启:不用 -ne,让所有扩展都加载
37
+ WORKDIR=$(mktemp -d -t aicode-ext.XXXXXX)
38
+ cat >"$WORKDIR/config.json" <<EOF
39
+ {"name":"aicode-test","banner":"","themeColor":"#49bccf","configDir":"$(basename "$WORKDIR")"}
40
+ EOF
41
+
42
+ LOG="$RESULTS_DIR/${CURRENT_CASE}.log"
43
+ : >"$LOG"
44
+
45
+ "$AICODE_BIN" --config "$WORKDIR" --mode rpc --no-session \
46
+ < /dev/null >"$LOG" 2>&1 &
47
+ EXT_PID=$!
48
+
49
+ # 等启动完成(5s 应该够)
50
+ sleep 5
51
+ kill "$EXT_PID" 2>/dev/null
52
+ wait "$EXT_PID" 2>/dev/null
53
+
54
+ # 错误关键字扫描
55
+ echo " scanning log for extension errors..."
56
+
57
+ fail_count=0
58
+ for pattern in "Failed to load extension" "Tool.*conflicts with" "Cannot find module" "SyntaxError" "TypeError.*Extension"; do
59
+ matches=$(grep -cE "$pattern" "$LOG" 2>/dev/null || echo 0)
60
+ if [[ "$matches" -gt 0 ]]; then
61
+ fail "无 $pattern 错误" "found $matches matches"
62
+ grep -E "$pattern" "$LOG" | head -3 | sed 's/^/ /'
63
+ fail_count=$((fail_count + matches))
64
+ else
65
+ pass "无 '$pattern' 错误"
66
+ fi
67
+ done
68
+
69
+ # ----- Test 2: get_commands 包含 aicode- 自定义命令 -----
70
+ # 不带 -ne 启动,确认扩展注册的 slash commands 也可见
71
+ header "$CURRENT_CASE" "aicode custom slash commands"
72
+
73
+ spawn_rpc 2>/dev/null || {
74
+ fail "second spawn" "进程启动失败"
75
+ summary; exit 1
76
+ }
77
+
78
+ send '{"type":"get_commands","id":"e1"}'
79
+ sleep 1
80
+
81
+ cmds_json=$(jq -c 'select(.id=="e1" and .type=="response") | .data.commands' "$REGRESSION_LOG" 2>/dev/null | head -1)
82
+ cmd_count=$(echo "$cmds_json" | jq -c 'length' 2>/dev/null)
83
+ echo " registered commands: $cmd_count"
84
+
85
+ if [[ -z "$cmd_count" || "$cmd_count" == "null" || "$cmd_count" -lt 5 ]]; then
86
+ fail "至少注册 5 个命令" "got: $cmd_count"
87
+ else
88
+ pass "注册 $cmd_count 个 slash commands"
89
+ fi
90
+
91
+ # 关键 aicode 自定义命令(在 /llama /model /settings 等内置之外的 aicode-*)
92
+ AICODE_CMDS=("/aicode-cd" "/grill-me" "/bugs" "/goal" "/plan" "/workflow" "/git-commit" "/feat" "/init-project" "/code-simplifier")
93
+ for cmd in "${AICODE_CMDS[@]}"; do
94
+ if echo "$cmds_json" | jq -e --arg c "$cmd" '.[] | select(.name == $c)' >/dev/null 2>&1; then
95
+ pass "扩展命令 $cmd 已注册"
96
+ else
97
+ # 可能该命令需要其他条件才注册,标记 skip
98
+ skip "扩展命令 $cmd 已注册" "未在 get_commands 中找到"
99
+ fi
100
+ done
101
+
102
+ # ----- Test 3: get_state 反映扩展注册的工具 -----
103
+ header "$CURRENT_CASE" "tools registered by extensions"
104
+
105
+ send '{"type":"get_state","id":"e2"}'
106
+ sleep 1
107
+
108
+ # 通过发 prompt 让模型思考,看 tool_execution_start 是否包含 aicode 扩展工具
109
+ # (aicode-safety-guard 注册的 safety_check / aicode-plan 注册的 todo_update 等)
110
+
111
+ send '{"type":"prompt","message":"just say hi","id":"e3"}'
112
+ sleep 8
113
+
114
+ # 收集所有出现过的 tool_name
115
+ all_tools=$(jq -r 'select(.type=="tool_execution_start") | .toolName // empty' "$REGRESSION_LOG" 2>/dev/null | sort -u)
116
+ echo " observed tools: $(echo "$all_tools" | tr '\n' ' ')"
117
+
118
+ # 内置工具一定在
119
+ for tool in read bash write edit; do
120
+ if echo "$all_tools" | grep -qx "$tool"; then
121
+ pass "内置工具 $tool 可调用"
122
+ else
123
+ # 单次 prompt 可能不调用
124
+ skip "内置工具 $tool 可调用" "本次 prompt 未触发"
125
+ fi
126
+ done
127
+
128
+ cleanup_rpc
129
+ summary
130
+ exit $?
@@ -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 $?