Merge release/v1.1.4 cleanup

This commit is contained in:
Haitao Pan 2026-06-12 14:50:10 +08:00
commit 58bbf890e0
11 changed files with 1 additions and 932 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
build/
dist/
.env
xworkmate-bridge
xworkmate-go-core-linux

View File

@ -13,13 +13,6 @@ type CapabilityCatalog struct {
ProviderProbeSummary []any `json:"providerProbeSummary"`
}
func (c *CapabilityCatalog) Update(providers []any, targets []any) {
c.mu.Lock()
defer c.mu.Unlock()
c.ProviderCatalog = providers
c.AvailableExecutionTargets = targets
}
func (c *CapabilityCatalog) Get() map[string]any {
c.mu.RLock()
defer c.mu.RUnlock()

View File

@ -1,50 +0,0 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"xworkmate-bridge/internal/service"
)
type Authenticator interface {
Authenticate(username, password string) error
}
type AuthHandler struct {
service Authenticator
}
func NewAuthHandler(svc Authenticator) *AuthHandler {
return &AuthHandler{service: svc}
}
func NewServiceAdapter(svc *service.AuthService) Authenticator {
return authServiceAdapter{service: svc}
}
func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var payload struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if err := h.service.Authenticate(payload.Username, payload.Password); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
type authServiceAdapter struct {
service *service.AuthService
}
func (a authServiceAdapter) Authenticate(username, password string) error {
return a.service.Authenticate(context.TODO(), username, password)
}

View File

@ -1,53 +0,0 @@
package handler
import (
"bytes"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
type fakeAuthenticator struct {
err error
}
func (f fakeAuthenticator) Authenticate(username, password string) error {
return f.err
}
func TestAuthHandlerRejectsInvalidJSON(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString("{"))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestAuthHandlerReturnsUnauthorizedOnServiceFailure(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{err: errors.New("invalid credentials")})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}
func TestAuthHandlerReturnsOKOnSuccess(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}

View File

@ -1,40 +0,0 @@
package handler
import (
"encoding/json"
"net/http"
"xworkmate-bridge/internal/service"
"xworkmate-bridge/internal/shared"
)
type TokenAuthHandler struct {
service *service.StaticTokenAuthService
}
func NewTokenAuthHandler(service *service.StaticTokenAuthService) *TokenAuthHandler {
return &TokenAuthHandler{service: service}
}
func (h *TokenAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.service == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32000, "auth service unavailable"))
return
}
token := r.Header.Get("Authorization")
if h.service.ValidateAuthorizationHeader(token) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"ok": true,
"type": "res",
"payload": map[string]any{"authenticated": true},
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32001, "unauthorized"))
}

View File

@ -1,34 +0,0 @@
package handler
import (
"net/http"
"net/http/httptest"
"testing"
"xworkmate-bridge/internal/service"
)
func TestTokenAuthHandlerServeHTTP(t *testing.T) {
h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "secret")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}
func TestTokenAuthHandlerRejectsUnauthorized(t *testing.T) {
h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}

View File

@ -1,146 +0,0 @@
package mounts
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
const (
codexManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK"
codexManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK"
opencodeManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK"
opencodeManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK"
)
var mcpServerSectionPattern = regexp.MustCompile(
`(?m)^\[mcp_servers\.[^\]]+\]`,
)
func countMCPSections(content string) int {
return len(mcpServerSectionPattern.FindAllStringIndex(content, -1))
}
func defaultCodexHome() string {
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" {
return ""
}
return filepath.Join(home, ".codex")
}
func defaultOpencodeHome() string {
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" {
return ""
}
return filepath.Join(home, ".opencode")
}
func defaultOpenClawHome() string {
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" {
return ""
}
return filepath.Join(home, ".openclaw")
}
func stripManagedBlock(content, startMarker, endMarker string) string {
if strings.TrimSpace(content) == "" {
return content
}
remaining := content
for {
start := strings.Index(remaining, startMarker)
if start < 0 {
break
}
end := strings.Index(remaining[start:], endMarker)
if end < 0 {
remaining = remaining[:start]
break
}
end += start
remaining = remaining[:start] + remaining[end+len(endMarker):]
}
return remaining
}
func mergeManagedBlock(content, block, startMarker, endMarker string) string {
preserved := strings.TrimRight(
stripManagedBlock(content, startMarker, endMarker),
"\n",
)
if preserved == "" {
return block + "\n"
}
return preserved + "\n\n" + block + "\n"
}
func buildCodexManagedMCPBlock(servers []ManagedMCPServer) string {
var buffer strings.Builder
buffer.WriteString(codexManagedMCPBlockStart)
buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n")
_, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano))
for _, server := range servers {
_, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID)
_, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command)
if len(server.Args) > 0 {
_, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args))
}
buffer.WriteString("\n")
}
buffer.WriteString(codexManagedMCPBlockEnd)
return strings.TrimRight(buffer.String(), "\n")
}
func buildOpencodeManagedMCPBlock(servers []ManagedMCPServer) string {
var buffer strings.Builder
buffer.WriteString(opencodeManagedMCPBlockStart)
buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n")
_, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano))
for _, server := range servers {
_, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID)
if strings.TrimSpace(server.URL) != "" {
_, _ = fmt.Fprintf(&buffer, "url = %q\n", strings.TrimSpace(server.URL))
} else {
buffer.WriteString("type = \"stdio\"\n")
_, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command)
if len(server.Args) > 0 {
_, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args))
}
}
buffer.WriteString("\n")
}
buffer.WriteString(opencodeManagedMCPBlockEnd)
return strings.TrimRight(buffer.String(), "\n")
}
func formatTOMLArray(items []string) string {
if len(items) == 0 {
return "[]"
}
var quoted []string
for _, item := range items {
quoted = append(quoted, fmt.Sprintf("%q", item))
}
return "[" + strings.Join(quoted, ", ") + "]"
}
func applyManagedBlock(configPath, block, startMarker, endMarker string) error {
configDir := filepath.Dir(configPath)
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
content, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return err
}
merged := mergeManagedBlock(string(content), block, startMarker, endMarker)
return os.WriteFile(configPath, []byte(merged), 0o644)
}

View File

@ -1,444 +0,0 @@
package mounts
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
type ManagedMCPServer struct {
ID string
Name string
Transport string
Command string
URL string
Args []string
Enabled bool
}
type Config struct {
AutoSync bool
UsesAris bool
ManagedMCPServers []ManagedMCPServer
}
type ArisInput struct {
Available bool
BundleVersion string
LLMChatServerPath string
SkillCount int
BridgeAvailable bool
Error string
}
type Request struct {
Config Config
AIGatewayURL string
ConfiguredCodexCLIPath string
CodexHome string
OpencodeHome string
OpenClawHome string
Aris ArisInput
}
type MountTargetState struct {
TargetID string
Label string
Available bool
SupportsSkills bool
SupportsMCP bool
SupportsAIGatewayInjection bool
DiscoveryState string
SyncState string
DiscoveredSkillCount int
DiscoveredMCPCount int
ManagedMCPCount int
Detail string
}
type Result struct {
MountTargets []MountTargetState
ArisBundleVersion string
ArisCompatStatus string
}
func Reconcile(request Request) Result {
states := []MountTargetState{
reconcileAris(request.Config, request.Aris),
reconcileCodex(
request.Config,
request.AIGatewayURL,
request.ConfiguredCodexCLIPath,
request.CodexHome,
),
reconcileCLIListTarget(
request.Config,
"claude",
"Claude",
[]string{"claude", "mcp", "list"},
),
reconcileCLIListTarget(
request.Config,
"gemini",
"Gemini",
[]string{"gemini", "mcp", "list"},
),
reconcileOpencode(request.Config, request.OpencodeHome),
reconcileOpenClaw(request.Config, request.OpenClawHome),
}
result := Result{
MountTargets: states,
ArisBundleVersion: strings.TrimSpace(request.Aris.BundleVersion),
ArisCompatStatus: "idle",
}
for _, state := range states {
if state.TargetID == "aris" {
result.ArisCompatStatus = state.SyncState
break
}
}
return result
}
func ResultMap(result Result) map[string]any {
rawTargets := make([]map[string]any, 0, len(result.MountTargets))
for _, target := range result.MountTargets {
rawTargets = append(rawTargets, map[string]any{
"targetId": target.TargetID,
"label": target.Label,
"available": target.Available,
"supportsSkills": target.SupportsSkills,
"supportsMcp": target.SupportsMCP,
"supportsAiGatewayInjection": target.SupportsAIGatewayInjection,
"discoveryState": target.DiscoveryState,
"syncState": target.SyncState,
"discoveredSkillCount": target.DiscoveredSkillCount,
"discoveredMcpCount": target.DiscoveredMCPCount,
"managedMcpCount": target.ManagedMCPCount,
"detail": target.Detail,
})
}
return map[string]any{
"mountTargets": rawTargets,
"arisBundleVersion": result.ArisBundleVersion,
"arisCompatStatus": result.ArisCompatStatus,
}
}
func reconcileAris(config Config, input ArisInput) MountTargetState {
state := placeholderState("aris", "ARIS", true, true, false)
if strings.TrimSpace(input.Error) != "" {
state.Available = false
state.DiscoveryState = "error"
state.SyncState = "error"
state.Detail = strings.TrimSpace(input.Error)
return state
}
if !input.Available {
state.DiscoveryState = "missing"
state.SyncState = "missing"
state.Detail = "Embedded ARIS bundle is unavailable."
return state
}
state.Available = true
state.DiscoveryState = "ready"
state.DiscoveredSkillCount = input.SkillCount
llmChatReady := strings.TrimSpace(input.LLMChatServerPath) != ""
if config.UsesAris && llmChatReady && input.BridgeAvailable {
state.SyncState = "ready"
state.DiscoveredMCPCount = 1
state.ManagedMCPCount = 1
state.Detail = "Embedded bundle " +
strings.TrimSpace(input.BundleVersion) +
" ready; XWorkmate Go core manages llm-chat and claude-review."
return state
}
state.SyncState = "embedded"
if llmChatReady {
state.DiscoveredMCPCount = 1
}
if llmChatReady {
state.Detail = "Embedded bundle extracted, but the XWorkmate Go core is not available yet."
} else {
state.Detail = "Embedded bundle extracted, but llm-chat metadata is missing."
}
return state
}
func reconcileCodex(
config Config,
aiGatewayURL string,
configuredCodexCLIPath string,
codexHome string,
) MountTargetState {
state := placeholderState("codex", "Codex", true, true, true)
available := codexAvailable(configuredCodexCLIPath)
configHome := strings.TrimSpace(codexHome)
if configHome == "" {
configHome = defaultCodexHome()
}
configPath := filepath.Join(configHome, "config.toml")
content, _ := os.ReadFile(configPath)
discovered := countMCPSections(string(content))
managedServers := enabledCodexServers(config.ManagedMCPServers)
if available && config.AutoSync && len(managedServers) > 0 {
_ = applyManagedBlock(
configPath,
buildCodexManagedMCPBlock(managedServers),
codexManagedMCPBlockStart,
codexManagedMCPBlockEnd,
)
}
state.Available = available
if available {
state.DiscoveryState = "ready"
} else {
state.DiscoveryState = "missing"
}
switch {
case !available:
state.SyncState = "missing"
case config.AutoSync:
state.SyncState = "ready"
default:
state.SyncState = "disabled"
}
state.DiscoveredMCPCount = discovered
state.ManagedMCPCount = len(managedServers)
state.Detail = "Codex is exposed through the bridge control plane.\n" +
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
return state
}
func reconcileCLIListTarget(
config Config,
targetID string,
label string,
command []string,
) MountTargetState {
state := placeholderState(targetID, label, true, true, true)
available := binaryExists(command[0])
discovered := 0
if available {
discovered = countListedEntries(command)
}
state.Available = available
if available {
state.DiscoveryState = "ready"
} else {
state.DiscoveryState = "missing"
}
if available && config.AutoSync {
state.SyncState = "launch-only"
} else {
state.SyncState = "disabled"
}
state.DiscoveredMCPCount = discovered
state.ManagedMCPCount = len(enabledServers(config.ManagedMCPServers))
if targetID == "gemini" {
state.Detail = "Gemini is exposed through the bridge control plane.\n" +
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
} else {
state.Detail = "MCP discovery uses `" + strings.Join(command, " ") +
"`; LLM API stays launch-scoped."
}
return state
}
func reconcileOpencode(config Config, opencodeHome string) MountTargetState {
state := placeholderState("opencode", "OpenCode", true, true, true)
available := binaryExists("opencode")
configHome := strings.TrimSpace(opencodeHome)
if configHome == "" {
configHome = defaultOpencodeHome()
}
configPath := filepath.Join(configHome, "config.toml")
content, _ := os.ReadFile(configPath)
discovered := countMCPSections(string(content))
managedServers := enabledServers(config.ManagedMCPServers)
if available && config.AutoSync && len(managedServers) > 0 {
_ = applyManagedBlock(
configPath,
buildOpencodeManagedMCPBlock(managedServers),
opencodeManagedMCPBlockStart,
opencodeManagedMCPBlockEnd,
)
}
state.Available = available
if available {
state.DiscoveryState = "ready"
} else {
state.DiscoveryState = "missing"
}
switch {
case !available:
state.SyncState = "missing"
case config.AutoSync:
state.SyncState = "ready"
default:
state.SyncState = "disabled"
}
state.DiscoveredMCPCount = discovered
state.ManagedMCPCount = len(managedServers)
state.Detail = "OpenCode is exposed through the bridge control plane.\n" +
"Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" +
"Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc"
return state
}
func reconcileOpenClaw(config Config, openClawHome string) MountTargetState {
state := placeholderState("openclaw", "OpenClaw", true, false, true)
available := binaryExists("openclaw")
state.Available = available
if available {
state.DiscoveryState = "ready"
} else {
state.DiscoveryState = "missing"
}
if available && config.AutoSync {
state.SyncState = "launch-only"
} else {
state.SyncState = "disabled"
}
state.Detail = "OpenClaw acts as the host/control plane mount."
configHome := strings.TrimSpace(openClawHome)
if configHome == "" {
configHome = defaultOpenClawHome()
}
configPath := filepath.Join(configHome, "openclaw.json")
if content, err := os.ReadFile(configPath); err == nil {
var decoded map[string]any
if err := json.Unmarshal(content, &decoded); err == nil {
agents := 0
if rawAgents, ok := decoded["agents"].(map[string]any); ok {
if rawList, ok := rawAgents["list"].([]any); ok {
agents = len(rawList)
}
}
skillsDir := filepath.Join(configHome, "skills")
if entries, err := os.ReadDir(skillsDir); err == nil {
state.DiscoveredSkillCount = len(entries)
}
state.Detail = "agents: " + itoa(agents) + " · skills: " +
itoa(state.DiscoveredSkillCount)
} else {
state.Detail = "OpenClaw config detected but could not be fully parsed."
}
}
return state
}
func placeholderState(
targetID string,
label string,
supportsSkills bool,
supportsMCP bool,
supportsAIGatewayInjection bool,
) MountTargetState {
return MountTargetState{
TargetID: targetID,
Label: label,
SupportsSkills: supportsSkills,
SupportsMCP: supportsMCP,
SupportsAIGatewayInjection: supportsAIGatewayInjection,
DiscoveryState: "idle",
SyncState: "idle",
}
}
func codexAvailable(configuredPath string) bool {
if strings.TrimSpace(configuredPath) != "" {
if _, err := os.Stat(strings.TrimSpace(configuredPath)); err == nil {
return true
}
}
return binaryExists("codex")
}
func binaryExists(command string) bool {
_, err := exec.LookPath(command)
return err == nil
}
func countListedEntries(command []string) int {
output := strings.TrimSpace(runCommand(command))
if output == "" ||
strings.Contains(output, "No MCP servers configured") ||
strings.Contains(output, "No MCP servers configured yet") ||
strings.Contains(output, "No MCP servers configured.") {
return 0
}
lines := strings.Split(output, "\n")
count := 0
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch {
case trimmed == "":
case strings.HasPrefix(trimmed, "Usage:"):
case strings.HasPrefix(trimmed, "┌"):
case strings.HasPrefix(trimmed, "│"):
case strings.HasPrefix(trimmed, "└"):
default:
count++
}
}
return count
}
func runCommand(command []string) string {
if len(command) == 0 {
return ""
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
output, err := cmd.CombinedOutput()
if err != nil && len(output) == 0 {
return ""
}
return string(output)
}
func enabledServers(servers []ManagedMCPServer) []ManagedMCPServer {
filtered := make([]ManagedMCPServer, 0, len(servers))
for _, server := range servers {
if !server.Enabled {
continue
}
filtered = append(filtered, server)
}
sort.SliceStable(filtered, func(i, j int) bool {
return filtered[i].ID < filtered[j].ID
})
return filtered
}
func enabledCodexServers(servers []ManagedMCPServer) []ManagedMCPServer {
filtered := make([]ManagedMCPServer, 0, len(servers))
for _, server := range servers {
if !server.Enabled || strings.TrimSpace(server.Command) == "" {
continue
}
filtered = append(filtered, server)
}
sort.SliceStable(filtered, func(i, j int) bool {
return filtered[i].ID < filtered[j].ID
})
return filtered
}
func itoa(value int) string {
return strconv.Itoa(value)
}

View File

@ -1,115 +0,0 @@
package mounts
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestReconcileCodexAppliesManagedBlockAndPreservesUserEntries(t *testing.T) {
tempDir := t.TempDir()
configuredBinary := filepath.Join(tempDir, "custom-codex")
if err := os.WriteFile(configuredBinary, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write configured binary: %v", err)
}
configPath := filepath.Join(tempDir, "config.toml")
if err := os.WriteFile(configPath, []byte(`
[mcp_servers.user_server]
command = "user-mcp"
`), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
result := Reconcile(Request{
Config: Config{
AutoSync: true,
ManagedMCPServers: []ManagedMCPServer{
{ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "7777"}, Enabled: true},
},
},
ConfiguredCodexCLIPath: configuredBinary,
CodexHome: tempDir,
})
content, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config: %v", err)
}
if !strings.Contains(string(content), `[mcp_servers.user_server]`) {
t.Fatalf("expected user entry preserved: %s", string(content))
}
if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) {
t.Fatalf("expected managed entry written: %s", string(content))
}
if strings.Count(string(content), codexManagedMCPBlockStart) != 1 {
t.Fatalf("expected single managed block: %s", string(content))
}
if result.MountTargets[1].ManagedMCPCount != 1 {
t.Fatalf("expected codex managed count 1, got %d", result.MountTargets[1].ManagedMCPCount)
}
}
func TestReconcileOpencodeAppliesManagedBlockAndPreservesUserEntries(t *testing.T) {
tempDir := t.TempDir()
binDir := t.TempDir()
originalPath := os.Getenv("PATH")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+originalPath)
if err := os.WriteFile(filepath.Join(binDir, "opencode"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write opencode binary: %v", err)
}
configPath := filepath.Join(tempDir, "config.toml")
if err := os.WriteFile(configPath, []byte(`
[model]
name = "user-default"
`), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
result := Reconcile(Request{
Config: Config{
AutoSync: true,
ManagedMCPServers: []ManagedMCPServer{
{ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "3001"}, Enabled: true},
},
},
OpencodeHome: tempDir,
})
content, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config: %v", err)
}
if !strings.Contains(string(content), `[model]`) {
t.Fatalf("expected user config preserved: %s", string(content))
}
if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) {
t.Fatalf("expected managed opencode entry written: %s", string(content))
}
if strings.Count(string(content), opencodeManagedMCPBlockStart) != 1 {
t.Fatalf("expected single opencode managed block: %s", string(content))
}
if result.MountTargets[4].ManagedMCPCount != 1 {
t.Fatalf("expected opencode managed count 1, got %d", result.MountTargets[4].ManagedMCPCount)
}
}
func TestReconcileArisReportsReadyWhenBundleAndBridgeAreAvailable(t *testing.T) {
result := Reconcile(Request{
Config: Config{UsesAris: true},
Aris: ArisInput{
Available: true,
BundleVersion: "test",
LLMChatServerPath: "mcp-server.py",
SkillCount: 2,
BridgeAvailable: true,
},
})
if got := result.MountTargets[0].SyncState; got != "ready" {
t.Fatalf("expected ready aris state, got %q", got)
}
if got := result.ArisBundleVersion; got != "test" {
t.Fatalf("expected bundle version test, got %q", got)
}
}

View File

@ -92,22 +92,3 @@ func NotificationEnvelope(method string, params map[string]any) map[string]any {
"seq": 0,
}
}
func ToolTextResult(id any, content string) map[string]any {
result := map[string]any{
"content": []map[string]any{
{"type": "text", "text": content},
},
}
return ResultEnvelope(id, result)
}
func ToolErrorResult(id any, err error) map[string]any {
result := map[string]any{
"content": []map[string]any{
{"type": "text", "text": fmt.Sprintf("Error: %v", err)},
},
"isError": true,
}
return ResultEnvelope(id, result)
}

View File

@ -342,30 +342,6 @@ func HandleChatTool(arguments map[string]any) (string, error) {
return CallOpenAICompatible(baseURL, apiKey, model, messages)
}
func HandleClaudeReviewTool(arguments map[string]any) (string, error) {
prompt := strings.TrimSpace(StringArg(arguments, "prompt", ""))
if prompt == "" {
return "", errors.New("prompt is required")
}
model := strings.TrimSpace(
StringArg(arguments, "model", EnvOrDefault("CLAUDE_REVIEW_MODEL", "")),
)
system := strings.TrimSpace(
StringArg(arguments, "system", EnvOrDefault("CLAUDE_REVIEW_SYSTEM", "")),
)
tools := strings.TrimSpace(
StringArg(arguments, "tools", EnvOrDefault("CLAUDE_REVIEW_TOOLS", "")),
)
timeout := IntArg(EnvOrDefault("CLAUDE_REVIEW_TIMEOUT_SEC", "600"), 600)
return RunClaudeReview(
prompt,
model,
system,
tools,
time.Duration(timeout)*time.Second,
)
}
func CallOpenAICompatible(
baseURL,
apiKey,