Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

272 lines
8.4 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/dbtest"
"go.kenn.io/agentsview/internal/service"
)
func TestNormalizeMCPHTTPAddr(t *testing.T) {
t.Parallel()
cases := []struct {
name string
addr string
allowInsecure bool
want string
wantErr bool
}{
{"empty", "", false, "", true},
{"bare port", "8085", false, "127.0.0.1:8085", false},
{"colon port", ":8085", false, "127.0.0.1:8085", false},
{"explicit loopback v4", "127.0.0.1:8085", false, "127.0.0.1:8085", false},
{"explicit loopback v6", "[::1]:8085", false, "[::1]:8085", false},
{"localhost", "localhost:8085", false, "localhost:8085", false},
{"non-loopback rejected", "192.168.1.5:8085", false, "", true},
{"all-interfaces rejected", "0.0.0.0:8085", false, "", true},
{"non-loopback opted in", "192.168.1.5:8085", true, "192.168.1.5:8085", false},
{"all-interfaces opted in", "0.0.0.0:8085", true, "0.0.0.0:8085", false},
{"not a port", "notaport", false, "", true},
// Empty host with a port still binds all interfaces, so it must be
// rejected without the opt-in.
{"empty host footgun", "[]:8085", false, "", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := normalizeMCPHTTPAddr(tc.addr, tc.allowInsecure)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestMCPListenerAuth(t *testing.T) {
t.Parallel()
// Loopback without require_auth is local-trust: no listener auth, even
// when a token happens to be configured.
tok, err := mcpListenerAuth("127.0.0.1:8085", "", false)
require.NoError(t, err)
assert.Empty(t, tok)
tok, err = mcpListenerAuth("[::1]:8085", "abc", false)
require.NoError(t, err)
assert.Empty(t, tok, "loopback bind does not enforce a token without require_auth")
// require_auth forces auth even on loopback, so a forwarded port is
// never an unauthenticated surface.
tok, err = mcpListenerAuth("127.0.0.1:8085", "abc", true)
require.NoError(t, err)
assert.Equal(t, "abc", tok, "require_auth enforces the token on loopback")
// require_auth on loopback without a token is refused.
_, err = mcpListenerAuth("127.0.0.1:8085", "", true)
require.Error(t, err)
assert.Contains(t, err.Error(), "auth token")
// Non-loopback with a token enforces it.
tok, err = mcpListenerAuth("192.168.1.5:8085", "abc", false)
require.NoError(t, err)
assert.Equal(t, "abc", tok)
// Non-loopback without a token is refused (no unauthenticated remote surface).
_, err = mcpListenerAuth("192.168.1.5:8085", "", false)
require.Error(t, err)
assert.Contains(t, err.Error(), "auth token")
}
func TestNewMCPCommand_Wiring(t *testing.T) {
t.Parallel()
cmd := newMCPCommand()
assert.Equal(t, "mcp", cmd.Use)
assert.Equal(t, groupData, cmd.GroupID)
assert.True(t, cmd.SilenceUsage)
for _, name := range []string{
"http", "http-allow-insecure", "server", "server-token-file", "pg",
} {
assert.NotNil(t, cmd.Flags().Lookup(name), "missing flag --%s", name)
}
}
func TestRootCommand_RegistersMCP(t *testing.T) {
t.Parallel()
root := newRootCommand()
var found bool
for _, c := range root.Commands() {
if c.Use == "mcp" {
found = true
break
}
}
assert.True(t, found, "root command should register the mcp subcommand")
}
func TestResolveMCPServicePGFlagUsesPGReadStore(t *testing.T) {
dataDir := newAgentDataDir(t)
remoteDir := t.TempDir()
t.Setenv("AGENTSVIEW_PG_URL", "postgres://example.test/agentsview")
t.Setenv("AGENTSVIEW_PG_SCHEMA", "custom_schema")
seedSession(t, dataDir, "local-session", "local")
seedSession(t, remoteDir, "pg-session", "remote")
remoteDB := dbtest.OpenTestDBAt(t, filepath.Join(remoteDir, "sessions.db"))
stub := stubPGReadStore(t, remoteDB)
forbidStartBackgroundServeForTransport(t,
"agentsview mcp --pg must use the PG read store, not the daemon")
cmd := newMCPCommand()
cmd.SetArgs([]string{"--pg"})
require.NoError(t, cmd.ParseFlags([]string{"--pg"}))
svc, cleanup, err := resolveMCPService(cmd)
require.NoError(t, err)
t.Cleanup(cleanup)
res, err := svc.List(context.Background(), service.ListFilter{Limit: 10})
require.NoError(t, err)
require.Len(t, res.Sessions, 1)
assert.Equal(t, "pg-session", res.Sessions[0].ID)
assert.Equal(t, "postgres://example.test/agentsview", stub.PG.URL)
assert.Equal(t, "custom_schema", stub.PG.Schema)
}
func TestMCPDaemonServiceStartsDaemonForEachOperation(t *testing.T) {
dataDir := t.TempDir()
cfg := config.Config{
DataDir: dataDir,
DBPath: filepath.Join(dataDir, "sessions.db"),
}
var starts int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/sessions", r.URL.Path)
assert.Equal(t, "7", r.URL.Query().Get("limit"))
_ = json.NewEncoder(w).Encode(service.SessionList{
Sessions: []db.Session{{ID: "from-daemon", Agent: "codex"}},
Total: 1,
})
}))
t.Cleanup(ts.Close)
host, port := splitTestServerURL(t, ts.URL)
stubStartBackgroundServeForTransport(t, func(
context.Context, *config.Config, time.Duration,
) (*DaemonRuntime, error) {
starts++
return &DaemonRuntime{Host: host, Port: port}, nil
})
svc := newMCPDaemonService(cfg)
for range 2 {
res, err := svc.List(context.Background(), service.ListFilter{Limit: 7})
require.NoError(t, err)
require.Len(t, res.Sessions, 1)
assert.Equal(t, "from-daemon", res.Sessions[0].ID)
}
assert.Equal(t, 2, starts)
assert.NoFileExists(t, cfg.DBPath)
}
func TestMCPDaemonService_UsagePairwiseComparisonForwardsToDaemon(t *testing.T) {
dataDir := t.TempDir()
cfg := config.Config{
DataDir: dataDir,
DBPath: filepath.Join(dataDir, "sessions.db"),
}
expected := service.UsagePairwiseComparisonResponse{
Left: service.UsagePairwiseComparisonSide{
TotalCost: 1.25,
TotalTokens: 150,
SessionCount: 2,
},
Right: service.UsagePairwiseComparisonSide{
TotalCost: 3.5,
TotalTokens: 420,
SessionCount: 5,
},
Deltas: service.UsagePairwiseComparisonDelta{
TotalCostDelta: 2.25,
TotalTokensDelta: 270,
SessionCountDelta: 3,
},
}
var starts int
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/usage/pairwise-comparison", r.URL.Path)
assert.Equal(t, "2024-06-01", r.URL.Query().Get("from"))
assert.Equal(t, "2024-06-07", r.URL.Query().Get("to"))
assert.Equal(t, "UTC", r.URL.Query().Get("timezone"))
assert.Equal(t, "gpt-4o", r.URL.Query().Get("model"))
assert.Equal(t, "model", r.URL.Query().Get("left_dimension"))
assert.Equal(t, "claude-sonnet-4-20250514", r.URL.Query().Get("left_value"))
assert.Equal(t, "project", r.URL.Query().Get("right_dimension"))
assert.Equal(t, "proj-b", r.URL.Query().Get("right_value"))
assert.Equal(t, "3", r.URL.Query().Get("min_user_messages"))
assert.Equal(t, "true", r.URL.Query().Get("include_one_shot"))
assert.Equal(t, "false", r.URL.Query().Get("include_automated"))
_ = json.NewEncoder(w).Encode(expected)
}))
t.Cleanup(ts.Close)
host, port := splitTestServerURL(t, ts.URL)
stubStartBackgroundServeForTransport(t, func(
context.Context, *config.Config, time.Duration,
) (*DaemonRuntime, error) {
starts++
return &DaemonRuntime{Host: host, Port: port}, nil
})
svc := newMCPDaemonService(cfg)
res, err := svc.UsagePairwiseComparison(
context.Background(),
service.UsagePairwiseComparisonRequest{
UsageRequest: service.UsageRequest{
From: "2024-06-01",
To: "2024-06-07",
Timezone: "UTC",
MinUserMessages: 3,
IncludeOneShot: true,
Model: "gpt-4o",
},
LeftDimension: "model",
LeftValue: "claude-sonnet-4-20250514",
RightDimension: "project",
RightValue: "proj-b",
},
)
require.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, expected, *res)
assert.Equal(t, 1, starts)
assert.NoFileExists(t, cfg.DBPath)
}
func splitTestServerURL(t *testing.T, raw string) (string, int) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, raw, nil)
require.NoError(t, err)
host, portText, err := net.SplitHostPort(req.URL.Host)
require.NoError(t, err)
var port int
_, err = fmt.Sscanf(portText, "%d", &port)
require.NoError(t, err)
return host, port
}