Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
439 lines
18 KiB
Go
439 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
agentstatusservice "github.com/tutti-os/tutti/services/tuttid/service/agentstatus"
|
|
)
|
|
|
|
type stubAgentStatusService struct {
|
|
listFn func(context.Context, agentstatusservice.ListInput) (agentstatusservice.Snapshot, error)
|
|
probeFn func(context.Context, agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error)
|
|
runActionFn func(context.Context, agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error)
|
|
catalogFn func(context.Context, string) (agentstatusservice.CodexRuntimeCatalog, error)
|
|
selectionFn func(context.Context, agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error)
|
|
}
|
|
|
|
func (s stubAgentStatusService) List(ctx context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
|
|
if s.listFn == nil {
|
|
return agentstatusservice.Snapshot{}, nil
|
|
}
|
|
return s.listFn(ctx, input)
|
|
}
|
|
|
|
func (s stubAgentStatusService) Probe(ctx context.Context, input agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error) {
|
|
if s.probeFn == nil {
|
|
return agentstatusservice.ProbeResult{}, nil
|
|
}
|
|
return s.probeFn(ctx, input)
|
|
}
|
|
|
|
func (s stubAgentStatusService) RunAction(ctx context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
|
|
if s.runActionFn == nil {
|
|
return agentstatusservice.RunActionResult{}, nil
|
|
}
|
|
return s.runActionFn(ctx, input)
|
|
}
|
|
|
|
func (s stubAgentStatusService) GetCodexRuntimeCatalog(ctx context.Context, provider string) (agentstatusservice.CodexRuntimeCatalog, error) {
|
|
if s.catalogFn == nil {
|
|
return agentstatusservice.CodexRuntimeCatalog{}, nil
|
|
}
|
|
return s.catalogFn(ctx, provider)
|
|
}
|
|
|
|
func (s stubAgentStatusService) SetCodexRuntimeSelection(ctx context.Context, input agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error) {
|
|
if s.selectionFn == nil {
|
|
return agentstatusservice.CodexRuntimeCatalog{}, nil
|
|
}
|
|
return s.selectionFn(ctx, input)
|
|
}
|
|
|
|
func TestDaemonAPIMapsCodexRuntimeCatalog(t *testing.T) {
|
|
capturedAt := time.Date(2026, 7, 28, 8, 0, 0, 0, time.UTC)
|
|
api := DaemonAPI{AgentStatusService: stubAgentStatusService{
|
|
catalogFn: func(_ context.Context, provider string) (agentstatusservice.CodexRuntimeCatalog, error) {
|
|
if provider != "codex" {
|
|
t.Fatalf("provider = %q, want codex", provider)
|
|
}
|
|
return agentstatusservice.CodexRuntimeCatalog{
|
|
CapturedAt: capturedAt,
|
|
Provider: "codex",
|
|
Revision: "catalog-revision",
|
|
Selection: agentstatusservice.CodexRuntimeSelection{State: agentstatusservice.CodexRuntimeSelectionSelectionRequired},
|
|
Candidates: []agentstatusservice.CodexRuntimeCatalogCandidate{{
|
|
ID: "candidate", LauncherPath: "/opt/bun/bin/codex", Sources: []string{"bun_global"}, State: "ready", AppServerReady: true, PackageLayoutOK: true,
|
|
}},
|
|
}, nil
|
|
},
|
|
}}
|
|
response, err := api.GetAgentProviderRuntimeCandidates(context.Background(), tuttigenerated.GetAgentProviderRuntimeCandidatesRequestObject{Provider: "codex"})
|
|
if err != nil {
|
|
t.Fatalf("GetAgentProviderRuntimeCandidates() error = %v", err)
|
|
}
|
|
got, ok := response.(tuttigenerated.GetAgentProviderRuntimeCandidates200JSONResponse)
|
|
if !ok || got.Revision != "catalog-revision" || len(got.Candidates) != 1 || got.Candidates[0].LauncherPath != "/opt/bun/bin/codex" {
|
|
t.Fatalf("response = %#v", response)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIForwardsCodexRuntimeSelection(t *testing.T) {
|
|
api := DaemonAPI{AgentStatusService: stubAgentStatusService{
|
|
selectionFn: func(_ context.Context, input agentstatusservice.SetCodexRuntimeSelectionInput) (agentstatusservice.CodexRuntimeCatalog, error) {
|
|
if input.Provider != "codex" || input.CandidateID != "candidate" || input.Revision != "revision" {
|
|
t.Fatalf("input = %#v", input)
|
|
}
|
|
return agentstatusservice.CodexRuntimeCatalog{Provider: "codex", Revision: "revision", Selection: agentstatusservice.CodexRuntimeSelection{State: agentstatusservice.CodexRuntimeSelectionSelected}}, nil
|
|
},
|
|
}}
|
|
candidateID, revision := "candidate", "revision"
|
|
response, err := api.SetAgentProviderRuntimeSelection(context.Background(), tuttigenerated.SetAgentProviderRuntimeSelectionRequestObject{
|
|
Provider: "codex",
|
|
Body: &tuttigenerated.SetAgentProviderRuntimeSelectionJSONRequestBody{
|
|
CandidateId: candidateID,
|
|
Revision: revision,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetAgentProviderRuntimeSelection() error = %v", err)
|
|
}
|
|
if _, ok := response.(tuttigenerated.SetAgentProviderRuntimeSelection200JSONResponse); !ok {
|
|
t.Fatalf("response = %#v", response)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesAgentProviderStatuses(t *testing.T) {
|
|
capturedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
|
|
if len(input.Providers) != 1 || input.Providers[0] != "claude-code" {
|
|
t.Fatalf("providers = %#v, want [claude-code]", input.Providers)
|
|
}
|
|
if !input.ForceRefresh {
|
|
t.Fatal("ForceRefresh = false, want true")
|
|
}
|
|
if !input.IncludeUpdates {
|
|
t.Fatal("IncludeUpdates = false, want true")
|
|
}
|
|
if !input.RefreshUpdates {
|
|
t.Fatal("RefreshUpdates = false, want true")
|
|
}
|
|
updateAvailable := true
|
|
return agentstatusservice.Snapshot{
|
|
CapturedAt: capturedAt,
|
|
Providers: []agentstatusservice.ProviderStatus{{
|
|
ActiveAction: &agentstatusservice.ActiveAction{
|
|
ID: agentstatusservice.ActionInstall,
|
|
Status: "running",
|
|
Step: "adapter",
|
|
Registry: "https://registry.example.test",
|
|
Stdout: "installing adapter\nstill installing",
|
|
},
|
|
Actions: []agentstatusservice.Action{{
|
|
ID: agentstatusservice.ActionRefresh,
|
|
Kind: agentstatusservice.ActionKindRefresh,
|
|
}},
|
|
Auth: agentstatusservice.AuthInfo{
|
|
Status: agentstatusservice.AuthUnknown,
|
|
},
|
|
Availability: agentstatusservice.Availability{
|
|
Status: agentstatusservice.AvailabilityUnknown,
|
|
},
|
|
CLI: agentstatusservice.CLIStatus{
|
|
Installed: true,
|
|
},
|
|
Update: agentstatusservice.UpdateStatus{
|
|
Capability: agentstatusservice.UpdateCapabilitySupported,
|
|
Source: agentstatusservice.UpdateSourceNPM,
|
|
CurrentVersion: "1.0.0",
|
|
LatestVersion: "1.1.0",
|
|
UpdateAvailable: &updateAvailable,
|
|
LastCheckedAt: &capturedAt,
|
|
},
|
|
Provider: "claude-code",
|
|
}},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=claude-code&includeUpdates=true&refreshUpdates=true&refresh=true", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.AgentProviderStatusListResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if !response.CapturedAt.Equal(capturedAt) {
|
|
t.Fatalf("capturedAt = %s, want %s", response.CapturedAt, capturedAt)
|
|
}
|
|
if len(response.Providers) != 1 {
|
|
t.Fatalf("providers length = %d, want 1", len(response.Providers))
|
|
}
|
|
if response.Providers[0].Provider != "claude-code" {
|
|
t.Fatalf("provider = %q, want claude-code", response.Providers[0].Provider)
|
|
}
|
|
activeAction := response.Providers[0].ActiveAction
|
|
if activeAction == nil {
|
|
t.Fatal("activeAction = nil, want install progress")
|
|
return
|
|
}
|
|
if activeAction.Phase != tuttigenerated.AgentProviderActiveActionPhaseInstall {
|
|
t.Fatalf("activeAction.phase = %q, want install", activeAction.Phase)
|
|
}
|
|
if activeAction.Registry == nil || *activeAction.Registry != "https://registry.example.test" {
|
|
t.Fatalf("activeAction.registry = %#v, want registry", activeAction.Registry)
|
|
}
|
|
if len(activeAction.Log) != 2 || activeAction.Log[0] != "installing adapter" {
|
|
t.Fatalf("activeAction.log = %#v, want split installer stdout", activeAction.Log)
|
|
}
|
|
update := response.Providers[0].Update
|
|
if update.Capability != tuttigenerated.AgentProviderUpdateCapabilitySupported || update.Source == nil || *update.Source != tuttigenerated.AgentProviderUpdateSourceNpm ||
|
|
update.CurrentVersion == nil || *update.CurrentVersion != "1.0.0" || update.LatestVersion == nil || *update.LatestVersion != "1.1.0" ||
|
|
update.UpdateAvailable == nil || !*update.UpdateAvailable || update.LastCheckedAt == nil {
|
|
t.Fatalf("update = %#v", update)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesUnsupportedAgentProviderStatus(t *testing.T) {
|
|
capturedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
|
|
if len(input.Providers) != 1 || input.Providers[0] != "hermes" {
|
|
t.Fatalf("providers = %#v, want [hermes]", input.Providers)
|
|
}
|
|
checkedAt := capturedAt
|
|
return agentstatusservice.Snapshot{
|
|
CapturedAt: capturedAt,
|
|
Providers: []agentstatusservice.ProviderStatus{{
|
|
Actions: []agentstatusservice.Action{},
|
|
Auth: agentstatusservice.AuthInfo{
|
|
Status: agentstatusservice.AuthUnknown,
|
|
},
|
|
Availability: agentstatusservice.Availability{
|
|
CheckedAt: &checkedAt,
|
|
ReasonCode: agentstatusservice.DisabledReasonProviderTemporarilyUnsupported,
|
|
Status: agentstatusservice.AvailabilityUnsupported,
|
|
},
|
|
Adapter: agentstatusservice.AdapterStatus{
|
|
Installed: false,
|
|
},
|
|
CLI: agentstatusservice.CLIStatus{
|
|
Installed: false,
|
|
},
|
|
Provider: "hermes",
|
|
}},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=hermes", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.AgentProviderStatusListResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if len(response.Providers) != 1 {
|
|
t.Fatalf("providers length = %d, want 1", len(response.Providers))
|
|
}
|
|
status := response.Providers[0]
|
|
if status.Provider != tuttigenerated.WorkspaceAgentProvider("hermes") {
|
|
t.Fatalf("provider = %q, want hermes", status.Provider)
|
|
}
|
|
if status.Availability.Status != tuttigenerated.AgentProviderAvailabilityStatusUnsupported {
|
|
t.Fatalf("availability = %q, want unsupported", status.Availability.Status)
|
|
}
|
|
if status.Availability.ReasonCode == nil || *status.Availability.ReasonCode != agentstatusservice.DisabledReasonProviderTemporarilyUnsupported {
|
|
t.Fatalf("reasonCode = %#v, want temporarily unsupported", status.Availability.ReasonCode)
|
|
}
|
|
if status.Cli.Installed || status.Adapter.Installed {
|
|
t.Fatalf("status = %#v, want CLI and adapter not installed", status)
|
|
}
|
|
if len(status.Actions) != 0 {
|
|
t.Fatalf("actions = %#v, want none", status.Actions)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesAgentProviderAuthIncludesAuthMethod(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
listFn: func(_ context.Context, input agentstatusservice.ListInput) (agentstatusservice.Snapshot, error) {
|
|
if len(input.Providers) != 1 || input.Providers[0] != "claude-code" {
|
|
t.Fatalf("providers = %#v, want [claude-code]", input.Providers)
|
|
}
|
|
return agentstatusservice.Snapshot{
|
|
Providers: []agentstatusservice.ProviderStatus{{
|
|
Actions: []agentstatusservice.Action{},
|
|
Auth: agentstatusservice.AuthInfo{
|
|
Status: agentstatusservice.AuthAuthenticated,
|
|
AccountLabel: "API Usage Billing",
|
|
AuthMethod: "apiKey",
|
|
},
|
|
Availability: agentstatusservice.Availability{
|
|
Status: agentstatusservice.AvailabilityReady,
|
|
},
|
|
CLI: agentstatusservice.CLIStatus{Installed: true, BinaryPath: "/usr/local/bin/claude"},
|
|
Adapter: agentstatusservice.AdapterStatus{Installed: true},
|
|
Provider: "claude-code",
|
|
}},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/agent-providers/status?providers=claude-code", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.AgentProviderStatusListResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if len(response.Providers) != 1 {
|
|
t.Fatalf("providers length = %d, want 1", len(response.Providers))
|
|
}
|
|
auth := response.Providers[0].Auth
|
|
if auth.Status != tuttigenerated.AgentProviderAuthStatusAuthenticated {
|
|
t.Fatalf("auth status = %q, want authenticated", auth.Status)
|
|
}
|
|
if auth.AccountLabel == nil || *auth.AccountLabel != "API Usage Billing" {
|
|
t.Fatalf("accountLabel = %#v, want API Usage Billing", auth.AccountLabel)
|
|
}
|
|
if auth.AuthMethod == nil || *auth.AuthMethod != "apiKey" {
|
|
t.Fatalf("authMethod = %#v, want apiKey", auth.AuthMethod)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesAgentProviderProbe(t *testing.T) {
|
|
checkedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
probeFn: func(_ context.Context, input agentstatusservice.ProbeInput) (agentstatusservice.ProbeResult, error) {
|
|
if input.Provider != "codex" {
|
|
t.Fatalf("provider = %q, want codex", input.Provider)
|
|
}
|
|
return agentstatusservice.ProbeResult{
|
|
Provider: "codex",
|
|
Status: agentstatusservice.ProbeFailed,
|
|
CheckedAt: checkedAt,
|
|
ReasonCode: "probe_exited",
|
|
Message: "adapter boom",
|
|
BinaryPath: "/usr/local/bin/codex-acp",
|
|
Command: []string{"/usr/local/bin/codex-acp"},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/probe", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.AgentProviderProbeResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if response.Status != tuttigenerated.AgentProviderProbeStatusFailed {
|
|
t.Fatalf("status = %q, want failed", response.Status)
|
|
}
|
|
if response.ReasonCode == nil || *response.ReasonCode != "probe_exited" {
|
|
t.Fatalf("reasonCode = %#v, want probe_exited", response.ReasonCode)
|
|
}
|
|
if response.Message == nil || *response.Message != "adapter boom" {
|
|
t.Fatalf("message = %#v, want adapter boom", response.Message)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesRunAgentProviderAction(t *testing.T) {
|
|
completedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
runActionFn: func(_ context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
|
|
if input.Provider != "codex" {
|
|
t.Fatalf("provider = %q, want codex", input.Provider)
|
|
}
|
|
if input.ActionID != agentstatusservice.ActionInstall {
|
|
t.Fatalf("actionID = %q, want install", input.ActionID)
|
|
}
|
|
return agentstatusservice.RunActionResult{
|
|
Provider: "codex",
|
|
ActionID: agentstatusservice.ActionInstall,
|
|
Status: agentstatusservice.RunActionFailed,
|
|
CompletedAt: completedAt,
|
|
ReasonCode: "post_install_probe_failed",
|
|
Message: "adapter boom",
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/codex/actions/install/run", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.AgentProviderActionRunResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if response.Status != tuttigenerated.AgentProviderActionRunStatusFailed {
|
|
t.Fatalf("status = %q, want failed", response.Status)
|
|
}
|
|
if response.ReasonCode == nil || *response.ReasonCode != "post_install_probe_failed" {
|
|
t.Fatalf("reasonCode = %#v, want post_install_probe_failed", response.ReasonCode)
|
|
}
|
|
if response.Message == nil || *response.Message != "adapter boom" {
|
|
t.Fatalf("message = %#v, want adapter boom", response.Message)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIRoutesRunAgentProviderUpdateAction(t *testing.T) {
|
|
completedAt := time.Date(2026, 6, 2, 8, 0, 0, 0, time.UTC)
|
|
mux := http.NewServeMux()
|
|
var readinessTrigger string
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentStatusService: stubAgentStatusService{
|
|
runActionFn: func(_ context.Context, input agentstatusservice.RunActionInput) (agentstatusservice.RunActionResult, error) {
|
|
if input.Provider != "tutti-agent" || input.ActionID != agentstatusservice.ActionUpdate {
|
|
t.Fatalf("input = %#v, want tutti-agent update", input)
|
|
}
|
|
return agentstatusservice.RunActionResult{
|
|
Provider: "tutti-agent",
|
|
ActionID: agentstatusservice.ActionUpdate,
|
|
Status: agentstatusservice.RunActionCompleted,
|
|
CompletedAt: completedAt,
|
|
}, nil
|
|
},
|
|
},
|
|
TuttiAgentReadiness: stubTuttiAgentReadiness{
|
|
providerActionCompletedFn: func(result agentstatusservice.RunActionResult) {
|
|
if result.Provider == "tutti-agent" && result.Status == agentstatusservice.RunActionCompleted {
|
|
readinessTrigger = "provider_action_completed"
|
|
}
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/agent-providers/tutti-agent/actions/update/run", nil)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
var response tuttigenerated.AgentProviderActionRunResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if response.ActionID != tuttigenerated.AgentProviderActionIDUpdate || response.Status != tuttigenerated.AgentProviderActionRunStatusCompleted {
|
|
t.Fatalf("response = %#v", response)
|
|
}
|
|
if readinessTrigger != "provider_action_completed" {
|
|
t.Fatalf("readiness trigger = %q, want provider_action_completed", readinessTrigger)
|
|
}
|
|
}
|