595 lines
17 KiB
Go
595 lines
17 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/agent/runtime/agentctl"
|
|
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
|
|
ws "github.com/kandev/kandev/pkg/websocket"
|
|
)
|
|
|
|
// mockExecutionLookup implements ExecutionLookup for testing.
|
|
type mockExecutionLookup struct {
|
|
executions map[string]*lifecycle.AgentExecution
|
|
// ensureErr allows tests to inject specific errors from GetOrEnsureExecution
|
|
ensureErr error
|
|
}
|
|
|
|
func (m *mockExecutionLookup) GetExecutionBySessionID(sessionID string) (*lifecycle.AgentExecution, bool) {
|
|
if m.executions == nil {
|
|
return nil, false
|
|
}
|
|
exec, ok := m.executions[sessionID]
|
|
return exec, ok
|
|
}
|
|
|
|
func (m *mockExecutionLookup) GetOrEnsureExecution(_ context.Context, sessionID string) (*lifecycle.AgentExecution, error) {
|
|
// Return injected error if set
|
|
if m.ensureErr != nil {
|
|
return nil, m.ensureErr
|
|
}
|
|
if m.executions == nil {
|
|
return nil, fmt.Errorf("no execution for session %s", sessionID)
|
|
}
|
|
exec, ok := m.executions[sessionID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no execution for session %s", sessionID)
|
|
}
|
|
return exec, nil
|
|
}
|
|
|
|
// mockSessionReader implements SessionReader for testing.
|
|
type mockSessionReader struct {
|
|
baseCommits map[string]string
|
|
baseBranches map[string]string
|
|
}
|
|
|
|
func (m *mockSessionReader) GetSessionBaseCommit(_ context.Context, sessionID string) string {
|
|
if m.baseCommits == nil {
|
|
return ""
|
|
}
|
|
return m.baseCommits[sessionID]
|
|
}
|
|
|
|
func (m *mockSessionReader) GetSessionBaseBranch(_ context.Context, sessionID string) string {
|
|
if m.baseBranches == nil {
|
|
return ""
|
|
}
|
|
return m.baseBranches[sessionID]
|
|
}
|
|
|
|
func TestNewGitHandlers(t *testing.T) {
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{}
|
|
reader := &mockSessionReader{}
|
|
|
|
h := NewGitHandlers(lookup, reader, log)
|
|
if h == nil {
|
|
t.Fatal("expected non-nil handlers")
|
|
}
|
|
if h.lifecycleMgr != lookup {
|
|
t.Error("expected lifecycleMgr to match")
|
|
}
|
|
if h.sessionReader != reader {
|
|
t.Error("expected sessionReader to match")
|
|
}
|
|
}
|
|
|
|
func TestNewGitHandlers_NilDependencies(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(nil, nil, log)
|
|
if h == nil {
|
|
t.Fatal("expected non-nil handlers")
|
|
}
|
|
}
|
|
|
|
func TestRegisterGitHandlers(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(nil, nil, log)
|
|
d := ws.NewDispatcher()
|
|
h.RegisterHandlers(d)
|
|
|
|
actions := []string{
|
|
ws.ActionWorktreePull,
|
|
ws.ActionWorktreePush,
|
|
ws.ActionWorktreeRebase,
|
|
ws.ActionWorktreeMerge,
|
|
ws.ActionWorktreeAbort,
|
|
ws.ActionWorktreeCommit,
|
|
ws.ActionWorktreeStage,
|
|
ws.ActionWorktreeUnstage,
|
|
ws.ActionWorktreeDiscard,
|
|
ws.ActionWorktreeCreatePR,
|
|
ws.ActionWorktreeRevertCommit,
|
|
ws.ActionWorktreeRenameBranch,
|
|
ws.ActionWorktreeReset,
|
|
ws.ActionSessionCommitDiff,
|
|
ws.ActionSessionGitCommits,
|
|
ws.ActionSessionCumulativeDiff,
|
|
}
|
|
for _, action := range actions {
|
|
if !d.HasHandler(action) {
|
|
t.Errorf("expected handler registered for %s", action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_NilResult(t *testing.T) {
|
|
log := newTestLogger()
|
|
called := false
|
|
h := NewGitHandlers(&mockExecutionLookup{}, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, _, _ string) {
|
|
called = true
|
|
})
|
|
|
|
// Should not panic or call callback
|
|
h.notifyGitOperationFailed("session-1", "commit", nil)
|
|
if called {
|
|
t.Error("callback should not be called for nil result")
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_SuccessResult(t *testing.T) {
|
|
log := newTestLogger()
|
|
called := false
|
|
h := NewGitHandlers(&mockExecutionLookup{}, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, _, _ string) {
|
|
called = true
|
|
})
|
|
|
|
h.notifyGitOperationFailed("session-1", "commit", &client.GitOperationResult{
|
|
Success: true,
|
|
})
|
|
if called {
|
|
t.Error("callback should not be called for successful result")
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_NilCallback(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(&mockExecutionLookup{}, nil, log)
|
|
// No callback set — should not panic
|
|
h.notifyGitOperationFailed("session-1", "commit", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "pre-commit hook failed",
|
|
})
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_NoExecution(t *testing.T) {
|
|
log := newTestLogger()
|
|
called := false
|
|
h := NewGitHandlers(&mockExecutionLookup{}, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, _, _ string) {
|
|
called = true
|
|
})
|
|
|
|
h.notifyGitOperationFailed("unknown-session", "commit", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "failed",
|
|
})
|
|
|
|
// Give the goroutine a moment (it shouldn't fire)
|
|
time.Sleep(50 * time.Millisecond)
|
|
if called {
|
|
t.Error("callback should not be called when no execution found")
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_EmptyTaskID(t *testing.T) {
|
|
log := newTestLogger()
|
|
called := false
|
|
lookup := &mockExecutionLookup{
|
|
executions: map[string]*lifecycle.AgentExecution{
|
|
"session-1": {ID: "exec-1", SessionID: "session-1", TaskID: ""},
|
|
},
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, _, _ string) {
|
|
called = true
|
|
})
|
|
|
|
h.notifyGitOperationFailed("session-1", "push", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "rejected",
|
|
})
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
if called {
|
|
t.Error("callback should not be called when task ID is empty")
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_UsesErrorField(t *testing.T) {
|
|
log := newTestLogger()
|
|
var mu sync.Mutex
|
|
var gotOperation, gotErrorOutput string
|
|
lookup := &mockExecutionLookup{
|
|
executions: map[string]*lifecycle.AgentExecution{
|
|
"session-1": {ID: "exec-1", SessionID: "session-1", TaskID: "task-1"},
|
|
},
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, operation, errorOutput string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
gotOperation = operation
|
|
gotErrorOutput = errorOutput
|
|
})
|
|
|
|
h.notifyGitOperationFailed("session-1", "commit", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "pre-commit hook failed",
|
|
Output: "some output",
|
|
})
|
|
|
|
// Wait for async callback
|
|
time.Sleep(100 * time.Millisecond)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if gotOperation != "commit" {
|
|
t.Errorf("expected operation 'commit', got %q", gotOperation)
|
|
}
|
|
if gotErrorOutput != "pre-commit hook failed" {
|
|
t.Errorf("expected error output 'pre-commit hook failed', got %q", gotErrorOutput)
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_FallsBackToOutput(t *testing.T) {
|
|
log := newTestLogger()
|
|
var mu sync.Mutex
|
|
var gotErrorOutput string
|
|
lookup := &mockExecutionLookup{
|
|
executions: map[string]*lifecycle.AgentExecution{
|
|
"session-1": {ID: "exec-1", SessionID: "session-1", TaskID: "task-1"},
|
|
},
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, _, _, _, errorOutput string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
gotErrorOutput = errorOutput
|
|
})
|
|
|
|
h.notifyGitOperationFailed("session-1", "push", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "",
|
|
Output: "fatal: rejected",
|
|
})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if gotErrorOutput != "fatal: rejected" {
|
|
t.Errorf("expected fallback to Output field, got %q", gotErrorOutput)
|
|
}
|
|
}
|
|
|
|
func TestNotifyGitOperationFailed_PassesSessionAndTaskID(t *testing.T) {
|
|
log := newTestLogger()
|
|
var mu sync.Mutex
|
|
var gotSessionID, gotTaskID string
|
|
lookup := &mockExecutionLookup{
|
|
executions: map[string]*lifecycle.AgentExecution{
|
|
"sess-42": {ID: "exec-1", SessionID: "sess-42", TaskID: "task-99"},
|
|
},
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
h.SetOnGitOperationFailed(func(_ context.Context, sessionID, taskID, _, _ string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
gotSessionID = sessionID
|
|
gotTaskID = taskID
|
|
})
|
|
|
|
h.notifyGitOperationFailed("sess-42", "rebase", &client.GitOperationResult{
|
|
Success: false,
|
|
Error: "conflict",
|
|
})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if gotSessionID != "sess-42" {
|
|
t.Errorf("expected sessionID 'sess-42', got %q", gotSessionID)
|
|
}
|
|
if gotTaskID != "task-99" {
|
|
t.Errorf("expected taskID 'task-99', got %q", gotTaskID)
|
|
}
|
|
}
|
|
|
|
func TestWsPull_InvalidPayload(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(nil, nil, log)
|
|
|
|
msg := &ws.Message{
|
|
ID: "test-1",
|
|
Action: ws.ActionWorktreePull,
|
|
Payload: json.RawMessage(`{invalid`),
|
|
}
|
|
|
|
_, err := h.wsPull(context.Background(), msg)
|
|
if err == nil {
|
|
t.Error("expected error for invalid payload")
|
|
}
|
|
}
|
|
|
|
func TestWsPull_MissingSessionID(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(nil, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionWorktreePull, GitPullRequest{SessionID: ""})
|
|
|
|
_, err := h.wsPull(context.Background(), msg)
|
|
if err == nil {
|
|
t.Error("expected error for missing session_id")
|
|
}
|
|
}
|
|
|
|
func TestWsCommit_MissingMessage(t *testing.T) {
|
|
log := newTestLogger()
|
|
h := NewGitHandlers(nil, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionWorktreeCommit, GitCommitRequest{
|
|
SessionID: "session-1",
|
|
Message: "",
|
|
})
|
|
|
|
_, err := h.wsCommit(context.Background(), msg)
|
|
if err == nil {
|
|
t.Error("expected error for missing message")
|
|
}
|
|
}
|
|
|
|
func TestWsPush_NoExecution(t *testing.T) {
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionWorktreePush, GitPushRequest{SessionID: "session-1"})
|
|
|
|
_, err := h.wsPush(context.Background(), msg)
|
|
if err == nil {
|
|
t.Error("expected error when no execution found")
|
|
}
|
|
}
|
|
|
|
func TestWsGitCommits_NotReady(t *testing.T) {
|
|
log := newTestLogger()
|
|
// Use "no agent running for session" error which matches isSessionNotReadyError
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: fmt.Errorf("no agent running for session session-1"),
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsGitCommits(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
commits, ok := payload["commits"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected commits array, got %T", payload["commits"])
|
|
}
|
|
if len(commits) != 0 {
|
|
t.Errorf("expected empty commits, got %d", len(commits))
|
|
}
|
|
}
|
|
|
|
func TestWsCumulativeDiff_NotReady(t *testing.T) {
|
|
log := newTestLogger()
|
|
// Use "no agent running for session" error which matches isSessionNotReadyError
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: fmt.Errorf("no agent running for session session-1"),
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionCumulativeDiff, CumulativeDiffRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsCumulativeDiff(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
if payload["cumulative_diff"] != nil {
|
|
t.Errorf("expected cumulative_diff=nil, got %v", payload["cumulative_diff"])
|
|
}
|
|
}
|
|
|
|
func TestWsCumulativeDiff_WorkspaceNotReady(t *testing.T) {
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: lifecycle.ErrSessionWorkspaceNotReady,
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionCumulativeDiff, CumulativeDiffRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsCumulativeDiff(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response for workspace not ready, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
if payload["cumulative_diff"] != nil {
|
|
t.Errorf("expected cumulative_diff=nil, got %v", payload["cumulative_diff"])
|
|
}
|
|
}
|
|
|
|
func TestWsCumulativeDiff_NilAgentClient(t *testing.T) {
|
|
log := newTestLogger()
|
|
// Execution exists but has no agentctl client yet
|
|
lookup := &mockExecutionLookup{
|
|
executions: map[string]*lifecycle.AgentExecution{
|
|
"session-1": {ID: "exec-1", SessionID: "session-1"},
|
|
},
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionCumulativeDiff, CumulativeDiffRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsCumulativeDiff(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response for nil agent client, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
if payload["cumulative_diff"] != nil {
|
|
t.Errorf("expected cumulative_diff=nil, got %v", payload["cumulative_diff"])
|
|
}
|
|
}
|
|
|
|
func TestWsCumulativeDiff_UnexpectedError(t *testing.T) {
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: errors.New("database connection failed"),
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionCumulativeDiff, CumulativeDiffRequest{SessionID: "session-1"})
|
|
|
|
_, err := h.wsCumulativeDiff(context.Background(), msg)
|
|
if err == nil {
|
|
t.Fatal("expected error for unexpected failure")
|
|
}
|
|
}
|
|
|
|
func TestIsSessionNotReadyError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{"nil error", nil, false},
|
|
{"no agent running", fmt.Errorf("no agent running for session abc"), true},
|
|
{"agent client not available", fmt.Errorf("agent client not available for session abc"), true},
|
|
{"unrelated error", fmt.Errorf("some other error"), false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := isSessionNotReadyError(tt.err); got != tt.want {
|
|
t.Errorf("isSessionNotReadyError() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWsGitCommits_WorkspaceNotReadyError(t *testing.T) {
|
|
// Test that ErrSessionWorkspaceNotReady returns ready:false gracefully
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: lifecycle.ErrSessionWorkspaceNotReady,
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsGitCommits(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response for workspace not ready, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
}
|
|
|
|
func TestWsGitCommits_UnexpectedError(t *testing.T) {
|
|
// Test that unexpected errors (not workspace-not-ready) are returned as errors
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: errors.New("database connection failed"),
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"})
|
|
|
|
_, err := h.wsGitCommits(context.Background(), msg)
|
|
if err == nil {
|
|
t.Fatal("expected error for unexpected failure")
|
|
}
|
|
if !errors.Is(err, lookup.ensureErr) && err.Error() != "failed to get execution for session session-1: database connection failed" {
|
|
t.Errorf("expected wrapped database error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWsGitCommits_WrappedWorkspaceNotReadyError(t *testing.T) {
|
|
// Test that wrapped ErrSessionWorkspaceNotReady is still detected
|
|
log := newTestLogger()
|
|
lookup := &mockExecutionLookup{
|
|
ensureErr: fmt.Errorf("some context: %w", lifecycle.ErrSessionWorkspaceNotReady),
|
|
}
|
|
h := NewGitHandlers(lookup, nil, log)
|
|
|
|
msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"})
|
|
|
|
resp, err := h.wsGitCommits(context.Background(), msg)
|
|
if err != nil {
|
|
t.Fatalf("expected graceful response for wrapped workspace not ready, got error: %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
|
t.Fatalf("failed to parse payload: %v", err)
|
|
}
|
|
if ready, ok := payload["ready"].(bool); !ok || ready {
|
|
t.Errorf("expected ready=false, got %v", payload["ready"])
|
|
}
|
|
}
|