2f8681b48e
Roadmap items 3-5 (session) and 3-6/7/8 (kb manage).
cli/cmd/session/ (new package; sessioncmd to avoid shadowing stdlib):
- session list: paginated table (ID/TITLE/UPDATED). --page / --page-size
with 1..1000 validation. _meta.has_more from page*size < total.
- session view <id>: prints metadata; non-empty fields only. Server
timestamps arrive as strings; parsed best-effort as RFC3339.
- session delete <id>: high-risk-write; exit-10 confirmation in non-
TTY/--json paths; --dry-run emits envelope.risk + dry_run:true.
cli/cmd/kb (extended):
- kb edit <id> [--name N] [--description D]: at least one flag required;
*string options so unset fields stay unset in the PUT body. SDK
UpdateKnowledgeBaseRequest has no embedding_model field, so the
roadmap's --embedding-model dropped.
- kb pin <id> / kb unpin <id>: direct parity with gh issue pin /
gh issue unpin (verified against gh manual). Idempotent: GetKnowledgeBase
reads IsPinned, TogglePinKnowledgeBase fires only on state change.
SDK KnowledgeBase struct gained the IsPinned field (server already
returned it; SDK just hadn't modeled it — non-breaking additive).
- kb empty <id>: high-risk-write; exit-10 confirmation;
--dry-run. Returns deleted_count from the async clear response.
weknora-specific operation; no mainstream parallel.
Golden envelopes for kb_list and kb_view updated to include the new
is_pinned field — strict-additive change.
Cleanups surfaced by the post-commit reviewer round:
- ConfirmPrompter promoted to cli/internal/testutil/ (4-copy threshold
reached: context/remove, kb/delete, kb/empty, session/delete).
kb/delete_test.go's pre-existing local copy left untouched per the
upstream-respect convention.
- kb pin/unpin idempotent no-op path no longer emits a write-class
envelope. Added _meta.warnings "already {un}pinned — no server
call made" and dropped the risk classification on the no-op branch.
- doc list --page-size was unbounded while session list enforces
1..1000. Same validation added to doc list.
18 + 18 unit tests; e2e exit codes verified.
Roadmap: 3-5, 3-6, 3-7, 3-8.
94 lines
2.9 KiB
Go
94 lines
2.9 KiB
Go
package kb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
|
|
"github.com/Tencent/WeKnora/cli/internal/iostreams"
|
|
"github.com/Tencent/WeKnora/cli/internal/testutil"
|
|
sdk "github.com/Tencent/WeKnora/client"
|
|
)
|
|
|
|
// fakeEmptySvc records calls + scripts the response.
|
|
type fakeEmptySvc struct {
|
|
err error
|
|
gotID string
|
|
called bool
|
|
resp *sdk.ClearKnowledgeBaseContentsResponse
|
|
}
|
|
|
|
func (f *fakeEmptySvc) ClearKnowledgeBaseContents(_ context.Context, id string) (*sdk.ClearKnowledgeBaseContentsResponse, error) {
|
|
f.called = true
|
|
f.gotID = id
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
if f.resp == nil {
|
|
return &sdk.ClearKnowledgeBaseContentsResponse{DeletedCount: 0}, nil
|
|
}
|
|
return f.resp, nil
|
|
}
|
|
|
|
func TestEmpty_WithYes(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeEmptySvc{resp: &sdk.ClearKnowledgeBaseContentsResponse{DeletedCount: 42}}
|
|
require.NoError(t, runEmpty(context.Background(), &EmptyOptions{Yes: true}, svc, &testutil.ConfirmPrompter{}, "kb_abc"))
|
|
assert.True(t, svc.called)
|
|
assert.Equal(t, "kb_abc", svc.gotID)
|
|
body := out.String()
|
|
assert.Contains(t, body, "kb_abc")
|
|
assert.Contains(t, body, "42")
|
|
}
|
|
|
|
func TestEmpty_NonTTY_NoYes_RequiresConfirmation(t *testing.T) {
|
|
iostreams.SetForTest(t)
|
|
svc := &fakeEmptySvc{}
|
|
err := runEmpty(context.Background(), &EmptyOptions{}, svc, &testutil.ConfirmPrompter{}, "kb_abc")
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputConfirmationRequired, typed.Code)
|
|
assert.Equal(t, 10, cmdutil.ExitCode(err))
|
|
assert.False(t, svc.called)
|
|
}
|
|
|
|
func TestEmpty_TTY_ConfirmNo(t *testing.T) {
|
|
_, errBuf := iostreams.SetForTestWithTTY(t)
|
|
svc := &fakeEmptySvc{}
|
|
p := &testutil.ConfirmPrompter{Answer: false}
|
|
err := runEmpty(context.Background(), &EmptyOptions{}, svc, p, "kb_abc")
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeUserAborted, typed.Code)
|
|
assert.False(t, svc.called)
|
|
assert.Contains(t, errBuf.String(), "Aborted")
|
|
}
|
|
|
|
func TestEmpty_NotFound(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeEmptySvc{err: errors.New("HTTP error 404: not found")}
|
|
err := runEmpty(context.Background(), &EmptyOptions{Yes: true}, svc, &testutil.ConfirmPrompter{}, "kb_missing")
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceNotFound, typed.Code)
|
|
}
|
|
|
|
func TestEmpty_DryRun_JSON(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeEmptySvc{}
|
|
require.NoError(t, runEmpty(context.Background(), &EmptyOptions{DryRun: true, JSONOut: true}, svc, &testutil.ConfirmPrompter{}, "kb_dry"))
|
|
body := out.String()
|
|
assert.True(t, strings.HasPrefix(body, `{"ok":true`))
|
|
assert.Contains(t, body, `"dry_run":true`)
|
|
assert.Contains(t, body, `"high-risk-write"`)
|
|
assert.False(t, svc.called)
|
|
}
|