mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-17 06:14:48 +00:00
* fix(cli): add missing X-GoClaw-User-Id header to gateway client * feat(acp): comprehensive integration with Gemini ACP protocol - Support nested JSON-RPC notification structures - Add robust streaming text collection and mapping - Increase handshake timeout to 60s for heavy model initialization - Fix WebSocket user_id authentication and schema v47 compatibility - Allow Google/GCP environment variables for ACP subprocesses * refactor(acp): multi-session architecture with session tracing and temp session cleanup - One shared Gemini process, multiple ACP sessions per process (one per goclaw conversation) - resolveSession: per-key mutex prevents TOCTOU race on concurrent session creation - Respawn detection via proc pointer comparison; session/load fallback after crash - sessionReaper: purges ACP sessions idle >30min; temp- sessions purged immediately on completion - WithGoclawSession context propagation: goclaw session key appears alongside ACP sid in all logs - dispatchUpdate: Gemini agent_message_chunk protocol mapping normalized here - session_test.go: full coverage of multi-session API (Initialize, NewSession, Prompt, Cancel, dispatch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(acp): align with ACP protocol standard (SDK v0.16.1) - InitializeRequest: "capabilities" → "clientCapabilities" (standard field name) - LoadSessionRequest: add mcpServers field (required by standard) - mapStopReason: add standard stop reasons (max_tokens, cancelled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(acp): address review findings from PR #901 - B1: Restore RequiredSchemaVersion to 55 (was incorrectly set to 47) - S1: Move X-GoClaw-User-Id header outside token check - S2: Add env gate (ACP_GEMINI_E2E) to Gemini E2E test - S3: Fix tab indentation in jsonrpc.go writeMessage - Build: Extract Pdeathsig to platform-specific files for cross-platform build * fix(acp): address S4-S7 review findings S4/S5: Send session/cancel before purging sessions locally - purgeSession() now cancels ACP session before deleting map entry - sessionReaper() sends cancel notification for idle sessions - Updated comments to reflect actual behavior S6: Tighten GOOGLE_/GCP_ env var filtering - Add GOOGLE_, GCP_ back to sensitiveEnvPrefixes - Add allowedEnvExact for safe vars: GOOGLE_API_KEY, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, GCP_PROJECT S7: Propagate ACP errors to callers - Chat/ChatStream now return err alongside ChatResponse - Enables upstream retry logic and error metrics --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: viettranx <viettranx@gmail.com>
60 lines
1.4 KiB
Go
60 lines
1.4 KiB
Go
package acp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGeminiProtocolMapping(t *testing.T) {
|
|
if os.Getenv("ACP_GEMINI_E2E") == "" {
|
|
t.Skip("set ACP_GEMINI_E2E=1 to run this test (requires gemini binary)")
|
|
}
|
|
// Enable debug logging to stderr
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
pp := NewProcessPool("gemini", []string{"--acp", "--yolo"}, ".", 10*time.Minute)
|
|
defer pp.Close()
|
|
|
|
proc, err := pp.GetOrSpawn(ctx, "test-session")
|
|
if err != nil {
|
|
t.Fatalf("Spawn failed: %v", err)
|
|
}
|
|
|
|
sid, err := proc.NewSession(ctx)
|
|
if err != nil {
|
|
t.Fatalf("NewSession failed: %v", err)
|
|
}
|
|
|
|
fmt.Println("-> Testing Gemini Protocol Mapping...")
|
|
|
|
var collectedText string
|
|
_, err = proc.Prompt(ctx, sid, []ContentBlock{
|
|
{Type: "text", Text: "Hello, reply with 'ACP_SUCCESS' and nothing else."},
|
|
}, func(su SessionUpdate) {
|
|
if su.Message != nil {
|
|
for _, b := range su.Message.Content {
|
|
if b.Type == "text" {
|
|
fmt.Printf("<- MAPPED CHUNK: %s\n", b.Text)
|
|
collectedText += b.Text
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Prompt failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("== COLLECTED TOTAL: %s ==\n", collectedText)
|
|
if collectedText == "" {
|
|
t.Error("No text collected via mapped Message field!")
|
|
}
|
|
}
|