mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-05-07 01:00:54 +08:00
Keep Claude Code compatibility work low-risk and reviewable
This change stops short of broader Claude Code runtime alignment and instead hardens two safe edges: builtin tool prefix handling and source-informed sentinel coverage for future drift checks. Constraint: Must preserve existing default behavior for current users Rejected: Implement control-plane/session alignment now | too much runtime risk for a first slice Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat the new fixtures as compatibility sentinels, not a full Claude Code schema contract Tested: go test ./test/...; go test ./sdk/translator/...; go test ./internal/runtime/executor -run 'Claude|Builtin|Tool'; go test ./... Not-tested: End-to-end Claude Code direct-connect/session runtime behavior
This commit is contained in:
106
test/claude_code_compatibility_sentinel_test.go
Normal file
106
test/claude_code_compatibility_sentinel_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type jsonObject = map[string]any
|
||||
|
||||
func loadClaudeCodeSentinelFixture(t *testing.T, name string) jsonObject {
|
||||
t.Helper()
|
||||
path := filepath.Join("testdata", "claude_code_sentinels", name)
|
||||
data := mustReadFile(t, path)
|
||||
var payload jsonObject
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
t.Fatalf("unmarshal %s: %v", name, err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func mustReadFile(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func requireStringField(t *testing.T, obj jsonObject, key string) string {
|
||||
t.Helper()
|
||||
value, ok := obj[key].(string)
|
||||
if !ok || value == "" {
|
||||
t.Fatalf("field %q missing or empty: %#v", key, obj[key])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ToolProgressShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "tool_progress.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "tool_progress" {
|
||||
t.Fatalf("type = %q, want tool_progress", got)
|
||||
}
|
||||
requireStringField(t, payload, "tool_use_id")
|
||||
requireStringField(t, payload, "tool_name")
|
||||
requireStringField(t, payload, "session_id")
|
||||
if _, ok := payload["elapsed_time_seconds"].(float64); !ok {
|
||||
t.Fatalf("elapsed_time_seconds missing or non-number: %#v", payload["elapsed_time_seconds"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_SessionStateShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "session_state_changed.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "system" {
|
||||
t.Fatalf("type = %q, want system", got)
|
||||
}
|
||||
if got := requireStringField(t, payload, "subtype"); got != "session_state_changed" {
|
||||
t.Fatalf("subtype = %q, want session_state_changed", got)
|
||||
}
|
||||
state := requireStringField(t, payload, "state")
|
||||
switch state {
|
||||
case "idle", "running", "requires_action":
|
||||
default:
|
||||
t.Fatalf("unexpected session state %q", state)
|
||||
}
|
||||
requireStringField(t, payload, "session_id")
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ToolUseSummaryShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "tool_use_summary.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "tool_use_summary" {
|
||||
t.Fatalf("type = %q, want tool_use_summary", got)
|
||||
}
|
||||
requireStringField(t, payload, "summary")
|
||||
rawIDs, ok := payload["preceding_tool_use_ids"].([]any)
|
||||
if !ok || len(rawIDs) == 0 {
|
||||
t.Fatalf("preceding_tool_use_ids missing or empty: %#v", payload["preceding_tool_use_ids"])
|
||||
}
|
||||
for i, raw := range rawIDs {
|
||||
if id, ok := raw.(string); !ok || id == "" {
|
||||
t.Fatalf("preceding_tool_use_ids[%d] invalid: %#v", i, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCodeSentinel_ControlRequestCanUseToolShape(t *testing.T) {
|
||||
payload := loadClaudeCodeSentinelFixture(t, "control_request_can_use_tool.json")
|
||||
if got := requireStringField(t, payload, "type"); got != "control_request" {
|
||||
t.Fatalf("type = %q, want control_request", got)
|
||||
}
|
||||
requireStringField(t, payload, "request_id")
|
||||
request, ok := payload["request"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("request missing or invalid: %#v", payload["request"])
|
||||
}
|
||||
if got := requireStringField(t, request, "subtype"); got != "can_use_tool" {
|
||||
t.Fatalf("request.subtype = %q, want can_use_tool", got)
|
||||
}
|
||||
requireStringField(t, request, "tool_name")
|
||||
requireStringField(t, request, "tool_use_id")
|
||||
if input, ok := request["input"].(map[string]any); !ok || len(input) == 0 {
|
||||
t.Fatalf("request.input missing or empty: %#v", request["input"])
|
||||
}
|
||||
}
|
||||
11
test/testdata/claude_code_sentinels/control_request_can_use_tool.json
vendored
Normal file
11
test/testdata/claude_code_sentinels/control_request_can_use_tool.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "control_request",
|
||||
"request_id": "req_123",
|
||||
"request": {
|
||||
"subtype": "can_use_tool",
|
||||
"tool_name": "Bash",
|
||||
"input": {"command": "npm test"},
|
||||
"tool_use_id": "toolu_123",
|
||||
"description": "Running npm test"
|
||||
}
|
||||
}
|
||||
7
test/testdata/claude_code_sentinels/session_state_changed.json
vendored
Normal file
7
test/testdata/claude_code_sentinels/session_state_changed.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "system",
|
||||
"subtype": "session_state_changed",
|
||||
"state": "requires_action",
|
||||
"uuid": "22222222-2222-4222-8222-222222222222",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
10
test/testdata/claude_code_sentinels/tool_progress.json
vendored
Normal file
10
test/testdata/claude_code_sentinels/tool_progress.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "tool_progress",
|
||||
"tool_use_id": "toolu_123",
|
||||
"tool_name": "Bash",
|
||||
"parent_tool_use_id": null,
|
||||
"elapsed_time_seconds": 2.5,
|
||||
"task_id": "task_123",
|
||||
"uuid": "11111111-1111-4111-8111-111111111111",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
7
test/testdata/claude_code_sentinels/tool_use_summary.json
vendored
Normal file
7
test/testdata/claude_code_sentinels/tool_use_summary.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "tool_use_summary",
|
||||
"summary": "Searched in auth/",
|
||||
"preceding_tool_use_ids": ["toolu_1", "toolu_2"],
|
||||
"uuid": "33333333-3333-4333-8333-333333333333",
|
||||
"session_id": "sess_123"
|
||||
}
|
||||
Reference in New Issue
Block a user