mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-08-29 02:04:30 +08:00
fix(cli): post-audit fixes (MCP MatchCount + sessions + auth + view)
Four unrelated shipped-code drifts found during v0.5 audit cycles: 1. MCP search_chunks tool omitted MatchCount from SearchParams. Server fell back to its default cap; agents asking for limit:50 silently got fewer results. Adds MatchCount: limit to the struct literal. 2. search sessions printed UpdatedAt as raw RFC3339 while session list used a fuzzy "X hours ago" render — same SDK field, two human renderings. Switches to the shared text.FuzzyAgoStr helper for parity. 3. auth status --json omitted three operationally-meaningful AuthUser fields (username, is_active, can_access_all_tenants). Agents branching on can_access_all_tenants previously needed a second round-trip. 4. session view Long help claimed the SDK doesn't wrap session_messages; it does (LoadMessages / GetMessagesBefore / GetRecentMessages all exist in client/message.go). Rewrites the comment to be accurate.
This commit is contained in:
+17
-7
@@ -14,7 +14,8 @@ import (
|
||||
// authStatusFields enumerates the fields surfaced for `--json` discovery
|
||||
// on `auth status`. Single-resource shape: filter applies to data itself.
|
||||
var authStatusFields = []string{
|
||||
"context", "user_id", "email", "tenant_id", "tenant_name",
|
||||
"context", "user_id", "username", "email", "is_active",
|
||||
"can_access_all_tenants", "tenant_id", "tenant_name",
|
||||
}
|
||||
|
||||
// StatusService is the narrow SDK surface auth status depends on.
|
||||
@@ -22,13 +23,19 @@ type StatusService interface {
|
||||
GetCurrentUser(ctx context.Context) (*sdk.CurrentUserResponse, error)
|
||||
}
|
||||
|
||||
// statusResult is the typed payload emitted by `--json`.
|
||||
// statusResult is the typed payload emitted by `--json`. Mirrors the
|
||||
// SDK AuthUser + AuthTenant projection so agents can branch on
|
||||
// can_access_all_tenants (cross-tenant admin) and is_active (disabled
|
||||
// account) without a second round-trip.
|
||||
type statusResult struct {
|
||||
Context string `json:"context"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
TenantID uint64 `json:"tenant_id,omitempty"`
|
||||
TenantName string `json:"tenant_name,omitempty"`
|
||||
Context string `json:"context"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
IsActive bool `json:"is_active,omitempty"`
|
||||
CanAccessAllTenants bool `json:"can_access_all_tenants,omitempty"`
|
||||
TenantID uint64 `json:"tenant_id,omitempty"`
|
||||
TenantName string `json:"tenant_name,omitempty"`
|
||||
}
|
||||
|
||||
// NewCmdStatus builds the `weknora auth status` command.
|
||||
@@ -80,7 +87,10 @@ func runStatus(ctx context.Context, jopts *cmdutil.JSONOptions, f *cmdutil.Facto
|
||||
result := statusResult{Context: cfg.CurrentContext}
|
||||
if user != nil {
|
||||
result.UserID = user.ID
|
||||
result.Username = user.Username
|
||||
result.Email = user.Email
|
||||
result.IsActive = user.IsActive
|
||||
result.CanAccessAllTenants = user.CanAccessAllTenants
|
||||
result.TenantID = user.TenantID
|
||||
}
|
||||
if tenant != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -105,12 +106,13 @@ done:
|
||||
}
|
||||
tw := tabwriter.NewWriter(iostreams.IO.Out, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tTITLE\tUPDATED")
|
||||
now := time.Now()
|
||||
for _, s := range matches {
|
||||
title := text.Truncate(50, s.Title)
|
||||
if title == "" {
|
||||
title = "-"
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", s.ID, title, s.UpdatedAt)
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", s.ID, title, text.FuzzyAgoStr(now, s.UpdatedAt))
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -75,3 +76,22 @@ func TestSessionsSearch_NetworkError(t *testing.T) {
|
||||
require.ErrorAs(t, err, &typed)
|
||||
assert.Equal(t, cmdutil.CodeServerError, typed.Code)
|
||||
}
|
||||
|
||||
// TestSessionsSearch_RendersFuzzyTime is a regression guard for the v0.5
|
||||
// audit bug: `search sessions` printed UpdatedAt as the raw RFC3339 string
|
||||
// while `session list` ran it through text.FuzzyAgoStr — same SDK field,
|
||||
// two human renderings. Asserts the human output now renders relative time
|
||||
// (and does NOT contain the RFC3339 "T" date/time separator).
|
||||
func TestSessionsSearch_RendersFuzzyTime(t *testing.T) {
|
||||
out, _ := iostreams.SetForTest(t)
|
||||
svc := &fakeSessionsSearchSvc{
|
||||
pages: map[int][]sdk.Session{1: {
|
||||
{ID: "s1", Title: "needle", UpdatedAt: time.Now().Add(-2 * time.Hour).Format(time.RFC3339)},
|
||||
}},
|
||||
total: 1,
|
||||
}
|
||||
require.NoError(t, runSessionsSearch(context.Background(), &SessionsSearchOptions{Query: "needle", Limit: 10}, nil, svc))
|
||||
body := out.String()
|
||||
assert.Contains(t, body, "hour", "must render relative time (e.g. 'about 2 hours ago'), not raw RFC3339")
|
||||
assert.NotContains(t, body, "T0", "raw RFC3339 has 'T' between date and time; fuzzyTime output should not")
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ type ViewService interface {
|
||||
GetSession(ctx context.Context, id string) (*sdk.Session, error)
|
||||
}
|
||||
|
||||
// NewCmdView builds `weknora session view <id>`. The server endpoint
|
||||
// returns metadata only (title/description/timestamps); message content
|
||||
// lives under a separate session_messages endpoint that the SDK doesn't
|
||||
// currently wrap, which is why there's no --full flag.
|
||||
// NewCmdView builds `weknora session view <id>`. Renders session metadata
|
||||
// only (title/description/timestamps). Full chat-history retrieval is a
|
||||
// separate concern (the SDK has LoadMessages / GetMessagesBefore for it);
|
||||
// surfacing it as `session view --full` is queued for v0.6.
|
||||
func NewCmdView(f *cmdutil.Factory) *cobra.Command {
|
||||
opts := &ViewOptions{}
|
||||
cmd := &cobra.Command{
|
||||
|
||||
@@ -283,6 +283,7 @@ func addSearchChunks(server *mcpsdk.Server, svc knowledgeService) {
|
||||
}
|
||||
results, err := svc.HybridSearch(ctx, in.KBID, &sdk.SearchParams{
|
||||
QueryText: in.Query,
|
||||
MatchCount: limit,
|
||||
VectorThreshold: in.VectorThreshold,
|
||||
KeywordThreshold: in.KeywordThreshold,
|
||||
})
|
||||
|
||||
@@ -334,6 +334,19 @@ func TestTool_SearchChunks_LimitCap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTool_SearchChunks_PassesMatchCountFromLimit is a regression guard for
|
||||
// the v0.5 audit bug: the search_chunks dispatch built SearchParams without
|
||||
// setting MatchCount, so the server fell back to its default cap and the
|
||||
// client-side trim (results[:limit]) was a no-op when limit > server default.
|
||||
// Verifies the limit arg is threaded into SearchParams.MatchCount.
|
||||
func TestTool_SearchChunks_PassesMatchCountFromLimit(t *testing.T) {
|
||||
svc := &fakeSvc{}
|
||||
c, _ := newTestServer(t, svc)
|
||||
callTool(t, c, "search_chunks", map[string]any{"kb_id": "kb_x", "query": "test", "limit": 50}, nil)
|
||||
require.NotNil(t, svc.calls.hybridParams, "HybridSearch must be called with non-nil SearchParams")
|
||||
assert.Equal(t, 50, svc.calls.hybridParams.MatchCount, "MCP search_chunks must thread limit into SearchParams.MatchCount")
|
||||
}
|
||||
|
||||
func TestTool_Chat_AccumulateAnswerAndReferences(t *testing.T) {
|
||||
svc := &fakeSvc{
|
||||
kbStreamEvents: []*sdk.StreamResponse{
|
||||
|
||||
Reference in New Issue
Block a user