@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,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 $?
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # 06-pi-new-features.sh
3
+ # 验证:pi 0.85+ 新功能在 aicode 端可用
4
+ #
5
+ # 重点:
6
+ # - clear_queue RPC(0.84.4+ 新增)
7
+ # - GPT-6 Astra 是否出现在 available_models
8
+ # - thinking 持久化(set_thinking_level 后 turn 间不丢)
9
+ # - usage.cacheWrite1h 字段(0.85+ 新增)
10
+ # - 持久化 Claude thinking effort(0.85+)
11
+ # - /thinking 命令选择器(0.84.3+)
12
+
13
+ set -uo pipefail
14
+ CURRENT_CASE="06-pi-new-features"
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$SCRIPT_DIR/../lib/common.sh"
17
+
18
+ header "$CURRENT_CASE" "pi 0.85+ new features"
19
+
20
+ if ! spawn_rpc 90; then
21
+ fail "spawn aicode RPC" "进程启动失败"
22
+ summary
23
+ exit 1
24
+ fi
25
+ pass "spawn aicode RPC (pid=$REGRESSION_PID)"
26
+
27
+ # ----- Test 1: clear_queue RPC -----
28
+ header "$CURRENT_CASE" "clear_queue RPC (0.84.4+)"
29
+
30
+ send '{"type":"clear_queue","id":"n1"}'
31
+ sleep 0.5
32
+
33
+ if jq -e 'select(.id=="n1" and .type=="response" and .success==true)' "$REGRESSION_LOG" >/dev/null 2>&1; then
34
+ pass "clear_queue RPC 成功"
35
+
36
+ # 验证返回结构
37
+ if jq -e 'select(.id=="n1" and .type=="response") | .data | has("steering") and has("followUp")' "$REGRESSION_LOG" >/dev/null 2>&1; then
38
+ pass "clear_queue 返回 {steering,followUp} 字段"
39
+ else
40
+ skip "clear_queue 返回 {steering,followUp} 字段" "字段名可能不同(followUps/follow_ups)"
41
+ fi
42
+ else
43
+ err=$(jq -c 'select(.id=="n1" and .type=="response") | .error // ""' "$REGRESSION_LOG" 2>/dev/null | head -1)
44
+ if [[ -n "$err" && "$err" != '""' ]]; then
45
+ skip "clear_queue RPC" "pi < 0.84.4 不支持: $err"
46
+ else
47
+ fail "clear_queue RPC" "无 response"
48
+ fi
49
+ fi
50
+
51
+ # ----- Test 2: GPT-6 Astra 在 available_models -----
52
+ header "$CURRENT_CASE" "GPT-6 Astra (0.85.0+)"
53
+
54
+ send '{"type":"get_available_models","id":"n2"}'
55
+ sleep 1
56
+
57
+ models=$(jq -c 'select(.id=="n2" and .type=="response") | .data' "$REGRESSION_LOG" 2>/dev/null | head -1)
58
+ if [[ -n "$models" ]]; then
59
+ if echo "$models" | jq -e '.[] | select(.id | tostring | contains("gpt-6"))' >/dev/null 2>&1; then
60
+ pass "GPT-6 系列模型在 catalog 中"
61
+ gpt6=$(echo "$models" | jq -r '.[] | select(.id | tostring | contains("gpt-6")) | .id' 2>/dev/null | head -3)
62
+ echo " 发现: $(echo "$gpt6" | tr '\n' ' ')"
63
+ else
64
+ skip "GPT-6 Astra 在 catalog 中" "未找到 gpt-6* 模型(取决于 provider 配置)"
65
+ fi
66
+ else
67
+ fail "get_available_models 返回 data" "无响应"
68
+ fi
69
+
70
+ # ----- Test 3: set_thinking_level + 持久化(0.85.0)-----
71
+ header "$CURRENT_CASE" "thinking level persistence (0.85.0)"
72
+
73
+ # 设置 high
74
+ send '{"type":"set_thinking_level","level":"high","id":"n3a"}'
75
+ sleep 0.5
76
+ if jq -e 'select(.id=="n3a" and .type=="response" and .success==true)' "$REGRESSION_LOG" >/dev/null 2>&1; then
77
+ pass "set_thinking_level high 成功"
78
+ else
79
+ fail "set_thinking_level high" "无响应"
80
+ fi
81
+
82
+ # 再发一个 prompt 让它跑
83
+ send '{"type":"prompt","message":"say hi","id":"n3b"}'
84
+ sleep 8
85
+
86
+ # 等 prompt 完成
87
+ waited=0
88
+ while [[ $waited -lt 60 ]]; do
89
+ if jq -e 'select(.id=="n3b" and .type=="response")' "$REGRESSION_LOG" >/dev/null 2>&1; then
90
+ break
91
+ fi
92
+ sleep 1
93
+ waited=$((waited + 1))
94
+ done
95
+
96
+ # 查询 get_state — thinking level 应该还是 high
97
+ send '{"type":"get_state","id":"n3c"}'
98
+ sleep 0.5
99
+
100
+ state_level=$(jq -c 'select(.id=="n3c" and .type=="response") | .data.thinkingLevel // "missing"' "$REGRESSION_LOG" 2>/dev/null | head -1)
101
+ if [[ "$state_level" == '"high"' ]]; then
102
+ pass "thinking level 在 turn 间保持 high(持久化)"
103
+ else
104
+ if [[ "$state_level" == '"medium"' ]]; then
105
+ skip "thinking level 持久化" "回到 medium(0.85 之前行为)"
106
+ else
107
+ fail "thinking level 持久化" "got: $state_level"
108
+ fi
109
+ fi
110
+
111
+ # ----- Test 4: usage.cacheWrite1h 字段(0.85.0+)-----
112
+ header "$CURRENT_CASE" "usage.cacheWrite1h field (0.85.0+)"
113
+
114
+ # 看 turn_end 事件的 usage 是否包含 cacheWrite1h
115
+ has_cw1h=$(jq -c 'select(.type=="turn_end") | .message.usage | has("cacheWrite1h")' "$REGRESSION_LOG" 2>/dev/null | head -1)
116
+ if [[ "$has_cw1h" == "true" ]]; then
117
+ pass "turn_end.usage.cacheWrite1h 字段存在"
118
+ else
119
+ skip "turn_end.usage.cacheWrite1h 字段" "不存在(pi < 0.85.0 或 provider 不报告)"
120
+ fi
121
+
122
+ # ----- Test 5: get_available_thinking_levels -----
123
+ header "$CURRENT_CASE" "thinking levels list"
124
+
125
+ send '{"type":"get_available_thinking_levels","id":"n5"}'
126
+ sleep 0.5
127
+
128
+ if jq -e 'select(.id=="n5" and .type=="response" and .success==true)' "$REGRESSION_LOG" >/dev/null 2>&1; then
129
+ levels=$(jq -c 'select(.id=="n5" and .type=="response") | .data | length' "$REGRESSION_LOG" 2>/dev/null | head -1)
130
+ if [[ -n "$levels" && "$levels" -gt 0 ]]; then
131
+ pass "get_available_thinking_levels 返回 $levels 个级别"
132
+ else
133
+ fail "thinking levels 非空" "got: $levels"
134
+ fi
135
+ else
136
+ fail "get_available_thinking_levels" "无响应"
137
+ fi
138
+
139
+ cleanup_rpc
140
+ summary
141
+ exit $?
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env bash
2
+ # common.sh - 回归测试公共工具
3
+ # 用法:在 case 脚本开头 source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
4
+
5
+ # 严格模式:未定义变量报错,但允许管道部分失败(grep 找不到时 exit 1)
6
+ set -uo pipefail
7
+
8
+ # 默认配置
9
+ : "${AICODE_BIN:=aicode}" # aicode 二进制路径
10
+ : "${AICODE_CONFIG:=$HOME/.aicode-cli}" # 配置目录
11
+ : "${AICODE_TIMEOUT:=30}" # RPC 测试默认超时(秒)
12
+ : "${REGRESSION_QUIET:=0}" # 1 = 减少输出
13
+
14
+ # 路径
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16
+ RESULTS_DIR="$SCRIPT_DIR/results"
17
+ mkdir -p "$RESULTS_DIR"
18
+
19
+ # 颜色(仅 TTY 启用)
20
+ if [[ -t 1 ]] && [[ "${REGRESSION_QUIET:-0}" != "1" ]]; then
21
+ RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[0;33m'; BLUE=$'\033[0;34m'; CYAN=$'\033[0;36m'; DIM=$'\033[2m'; NC=$'\033[0m'
22
+ else
23
+ RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; DIM=''; NC=''
24
+ fi
25
+
26
+ # 计数器
27
+ PASS_COUNT=0
28
+ FAIL_COUNT=0
29
+ SKIP_COUNT=0
30
+ CURRENT_CASE="${CURRENT_CASE:-$(basename "${BASH_SOURCE[1]:-unknown}" .sh)}"
31
+
32
+ # ----- 输出 -----
33
+ header() {
34
+ [[ "${REGRESSION_QUIET:-0}" == "1" ]] && return
35
+ echo ""
36
+ echo "${CYAN}─── $CURRENT_CASE: $2 ───${NC}"
37
+ }
38
+
39
+ pass() {
40
+ local name="$1"
41
+ echo " ${GREEN}✓${NC} $name"
42
+ PASS_COUNT=$((PASS_COUNT + 1))
43
+ }
44
+
45
+ fail() {
46
+ local name="$1"
47
+ local reason="${2:-no reason}"
48
+ echo " ${RED}✗${NC} $name"
49
+ echo " ${DIM}└─ $reason${NC}"
50
+ FAIL_COUNT=$((FAIL_COUNT + 1))
51
+ }
52
+
53
+ skip() {
54
+ local name="$1"
55
+ local reason="${2:-no reason}"
56
+ echo " ${YELLOW}○${NC} $name ${DIM}(skipped: $reason)${NC}"
57
+ SKIP_COUNT=$((SKIP_COUNT + 1))
58
+ }
59
+
60
+ summary() {
61
+ local exit_code=0
62
+ echo ""
63
+ if [[ $FAIL_COUNT -eq 0 && $SKIP_COUNT -eq 0 ]]; then
64
+ echo "${GREEN}✓ $CURRENT_CASE: ALL PASS${NC} ${DIM}($PASS_COUNT checks)${NC}"
65
+ elif [[ $FAIL_COUNT -eq 0 ]]; then
66
+ echo "${YELLOW}△ $CURRENT_CASE: PASS with skips${NC} ${DIM}($PASS_COUNT pass, $SKIP_COUNT skip)${NC}"
67
+ else
68
+ echo "${RED}✗ $CURRENT_CASE: FAILED${NC} ${DIM}($FAIL_COUNT fail, $PASS_COUNT pass, $SKIP_COUNT skip)${NC}"
69
+ exit_code=1
70
+ fi
71
+ return $exit_code
72
+ }
73
+
74
+ # ----- RPC 进程管理 -----
75
+ #
76
+ # spawn_rpc [timeout_seconds]
77
+ # 启动 aicode -ne --mode rpc --no-session,stdin 从 fd 11 读,stdout/stderr 到 fd 12 写
78
+ # 用法见 cases/02-rpc-handshake.sh
79
+ #
80
+ spawn_rpc() {
81
+ local timeout="${1:-$AICODE_TIMEOUT}"
82
+ local skip_extensions="${2:-true}"
83
+
84
+ if ! command -v "$AICODE_BIN" >/dev/null 2>&1; then
85
+ echo "ERROR: $AICODE_BIN not found. Set AICODE_BIN env var to override." >&2
86
+ return 127
87
+ fi
88
+
89
+ # 临时配置必须在 $HOME 下,aicode-cli 的 config-loader.ts 安全检查
90
+ # 会拒绝非 home 目录并回退到 ~/.aicode-cli
91
+ # 用 home 下的隐藏目录避免污染用户配置
92
+ local workdir="$HOME/.aicode-regression-$$-$(date +%s%N | tail -c 8)"
93
+ mkdir -p "$workdir/agent/extensions"
94
+ local cfg="$workdir/config.json"
95
+ cat >"$cfg" <<EOF
96
+ {
97
+ "name": "aicode-regression",
98
+ "banner": "",
99
+ "themeColor": "#49bccf",
100
+ "configDir": "$(basename "$workdir")"
101
+ }
102
+ EOF
103
+
104
+ # 复用用户全局的 auth.json / models.json / settings.json + prompts / skills / extensions
105
+ # (避免重新登录/配置,且能让 pi 加载预置 prompts 和 skills)
106
+ # ~/.pi 通常是 ~/.aicode-cli 的 symlink
107
+ local user_agent_dir="$HOME/.aicode-cli/agent"
108
+ if [[ -d "$user_agent_dir" ]]; then
109
+ for f in auth.json models.json settings.json; do
110
+ [[ -f "$user_agent_dir/$f" ]] && cp "$user_agent_dir/$f" "$workdir/agent/$f" 2>/dev/null
111
+ done
112
+ # 复制 prompts/skills/extensions 目录(不覆盖本地)
113
+ for d in prompts skills extensions themes; do
114
+ if [[ -d "$user_agent_dir/$d" ]]; then
115
+ cp -R "$user_agent_dir/$d" "$workdir/agent/" 2>/dev/null
116
+ fi
117
+ done
118
+ fi
119
+
120
+ # 覆盖 provider(如果设置了 AICODE_REGRESSION_PROVIDER_*)
121
+ # 用于跨供应商测试:minimax / siliconflow-anthropic / siliconflow-openai 等
122
+ if [[ -n "${AICODE_REGRESSION_BASE_URL:-}" ]] && [[ -n "${AICODE_REGRESSION_API_KEY:-}" ]]; then
123
+ # 在 models.json 里加一个 anthropic 兼容 provider
124
+ local api_kind="${AICODE_REGRESSION_API_KIND:-anthropic-messages}"
125
+ local provider_id="${AICODE_REGRESSION_PROVIDER_ID:-regression}"
126
+ local model_id="${AICODE_REGRESSION_MODEL_ID:-claude-sonnet-4-5}"
127
+ python3 -c "
128
+ import json
129
+ p = '$workdir/agent/models.json'
130
+ try:
131
+ d = json.load(open(p))
132
+ except: d = {'providers': {}}
133
+ prov = d.setdefault('providers', {})
134
+ prov['$provider_id'] = {
135
+ 'baseUrl': '$AICODE_REGRESSION_BASE_URL',
136
+ 'apiKey': '$AICODE_REGRESSION_API_KEY',
137
+ 'api': '$api_kind',
138
+ 'models': [
139
+ {'id': '$model_id', 'name': '$model_id', 'contextWindow': 128000, 'maxTokens': 16384}
140
+ ]
141
+ }
142
+ json.dump(d, open(p, 'w'), indent=2)
143
+ " 2>/dev/null
144
+
145
+ # 修改 settings.json 的 defaultProvider
146
+ [[ -f "$workdir/agent/settings.json" ]] && python3 -c "
147
+ import json
148
+ p = '$workdir/agent/settings.json'
149
+ try:
150
+ d = json.load(open(p))
151
+ except: d = {}
152
+ d['defaultProvider'] = '$provider_id'
153
+ d['defaultModel'] = '$model_id'
154
+ json.dump(d, open(p, 'w'), indent=2)
155
+ " 2>/dev/null
156
+ fi
157
+
158
+ # 创建 stdin fifo(aicode 需要从 stdin 持续读取 RPC 命令)
159
+ mkfifo "$workdir/stdin.fifo" 2>/dev/null || true
160
+
161
+ # fd 11 = stdin (case 脚本可以 printf 到 &11)
162
+ # fd 12 = stdout+stderr 合并的 log(用于后续分析)
163
+ exec 11<>"$workdir/stdin.fifo"
164
+ exec 12>"$workdir/output.log"
165
+
166
+ # spawn 进程(默认 -ne 跳过扩展避免 conflict;要加载扩展传 false)
167
+ local ext_flag=""
168
+ if [[ "$skip_extensions" == "true" ]]; then
169
+ ext_flag="-ne"
170
+ fi
171
+
172
+ "$AICODE_BIN" $ext_flag --config "$workdir" --mode rpc --no-session \
173
+ <&11 >&12 2>&1 &
174
+ local pid=$!
175
+
176
+ echo "$workdir" > "$RESULTS_DIR/.last_workdir"
177
+ echo "$pid" > "$RESULTS_DIR/.last_pid"
178
+
179
+ # 主动发一个 get_state 触发首个 response,验证 RPC 通道已就绪
180
+ printf '{"type":"get_state","id":"ready_probe"}\n' >&11
181
+
182
+ # 等待 response 出现(最多 5 秒)
183
+ local ready=0
184
+ for _ in $(seq 1 50); do
185
+ if grep -q '"id":"ready_probe"' "$workdir/output.log" 2>/dev/null; then
186
+ ready=1; break
187
+ fi
188
+ sleep 0.1
189
+ done
190
+
191
+ if [[ $ready -ne 1 ]]; then
192
+ kill "$pid" 2>/dev/null
193
+ echo "ERROR: aicode RPC 启动超时(未收到 ready_probe 响应)" >&2
194
+ echo " log 前 20 行:$(head -20 "$workdir/output.log" 2>/dev/null | tr '\n' '|' | head -c 300)" >&2
195
+ return 1
196
+ fi
197
+
198
+ REGRESSION_PID="$pid"
199
+ REGRESSION_WORKDIR="$workdir"
200
+ REGRESSION_LOG="$workdir/output.log"
201
+ return 0
202
+ }
203
+
204
+ send() {
205
+ # send <json>
206
+ printf '%s\n' "$1" >&11
207
+ }
208
+
209
+ wait_for() {
210
+ # wait_for <jq-filter> [timeout_seconds]
211
+ local filter="$1"
212
+ local timeout="${2:-$AICODE_TIMEOUT}"
213
+ local jqbin="${JQBIN:-jq}"
214
+
215
+ if ! command -v "$jqbin" >/dev/null 2>&1; then
216
+ echo "ERROR: jq not found. brew install jq" >&2
217
+ return 127
218
+ fi
219
+
220
+ # 等到匹配事件出现
221
+ "$jqbin" -c "$filter" "$REGRESSION_LOG" 2>/dev/null | head -1
222
+ }
223
+
224
+ cleanup_rpc() {
225
+ # 关掉 stdin pipe + 杀进程 + 保留 log
226
+ [[ -n "${REGRESSION_PID:-}" ]] && kill "$REGRESSION_PID" 2>/dev/null
227
+ exec 11<&- 2>/dev/null
228
+ exec 12<&- 2>/dev/null
229
+
230
+ if [[ -n "${REGRESSION_LOG:-}" ]] && [[ -f "$REGRESSION_LOG" ]]; then
231
+ cp "$REGRESSION_LOG" "$RESULTS_DIR/${CURRENT_CASE}.log" 2>/dev/null
232
+ fi
233
+ [[ -n "${REGRESSION_WORKDIR:-}" ]] && rm -rf "$REGRESSION_WORKDIR" 2>/dev/null
234
+ }
235
+
236
+ trap_cleanup() {
237
+ cleanup_rpc
238
+ }
239
+
240
+ # ----- 断言辅助 -----
241
+
242
+ assert_eq() {
243
+ # assert_eq <name> <expected> <actual>
244
+ local name="$1" expected="$2" actual="$3"
245
+ if [[ "$expected" == "$actual" ]]; then
246
+ pass "$name"
247
+ else
248
+ fail "$name" "expected: $expected | got: $actual"
249
+ fi
250
+ }
251
+
252
+ assert_contains() {
253
+ # assert_contains <name> <substring> <haystack>
254
+ local name="$1" needle="$2" hay="$3"
255
+ if [[ "$hay" == *"$needle"* ]]; then
256
+ pass "$name"
257
+ else
258
+ fail "$name" "expected to contain: $needle | got: ${hay:0:200}"
259
+ fi
260
+ }
261
+
262
+ assert_event_field() {
263
+ # assert_event_field <name> <jq-filter> <field-equals-value>
264
+ # 例:assert_event_field "model 是 anthropic" 'select(.type=="message_start")' '.message.api' "anthropic-messages"
265
+ local name="$1" event_filter="$2" field="$3" expected="$4"
266
+ local jqbin="${JQBIN:-jq}"
267
+ local actual
268
+ actual=$("$jqbin" -c "$event_filter | $field // \"\"" "$REGRESSION_LOG" 2>/dev/null | head -1)
269
+ if [[ "$actual" == *"\"$expected\""* ]] || [[ "$actual" == "\"$expected\"" ]]; then
270
+ pass "$name"
271
+ else
272
+ fail "$name" "expected $field == '$expected' | got: $actual"
273
+ fi
274
+ }
275
+
276
+ assert_event_count() {
277
+ # assert_event_count <name> <jq-filter> <min-count>
278
+ local name="$1" filter="$2" min="$3"
279
+ local jqbin="${JQBIN:-jq}"
280
+ local count
281
+ count=$("$jqbin" -c "$filter" "$REGRESSION_LOG" 2>/dev/null | wc -l | tr -d ' ')
282
+ if [[ $count -ge $min ]]; then
283
+ pass "$name ($count ≥ $min)"
284
+ else
285
+ fail "$name" "expected ≥ $min matches | got: $count"
286
+ fi
287
+ }
@@ -0,0 +1 @@
1
+ 42805
@@ -0,0 +1 @@
1
+ /Users/jianjiang/.aicode-regression-42781-8293000