8a0674186e
Add the resource-management surface to the v0.2 CLI: - weknora kb create --name X [--description Y] [--embedding-model Z] - weknora kb delete <id> [--force] - weknora doc list [--kb-id X | --kb NAME] [--page N] [--page-size M] - weknora doc upload <file> [--kb-id X | --kb NAME] [--name custom] - weknora doc delete <id> [--force] doc/* uses Factory.ResolveKB so the cwd's project link is honored when --kb-id is omitted. doc upload validates path existence with os.Stat (rejects directories; follows symlinks to mirror SDK os.Open behavior). doc list sorts by updated_at desc so newer items surface first. Both delete commands route through cmdutil.ConfirmDestructive — the "destructive op needs explicit user opt-in" pattern was about to be copy-pasted across the new subtree, so it was extracted with the delete commands as their first consumer. Saves the same dedup pass when v0.3 adds session/agent delete. (PR-12 later renames the flag from --force to the global -y/--yes for gh-style consistency.) iostreams.SetForTestWithTTY pairs with the existing SetForTest helper: the latter never reports stdout as a TTY (singleton replacement uses an in-memory buffer), so the confirm-yes / confirm-no test branches need a TTY-on variant.
125 lines
3.8 KiB
Go
125 lines
3.8 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"
|
|
sdk "github.com/Tencent/WeKnora/client"
|
|
)
|
|
|
|
// fakeCreateSvc captures the request and returns canned responses.
|
|
type fakeCreateSvc struct {
|
|
resp *sdk.KnowledgeBase
|
|
err error
|
|
got *sdk.KnowledgeBase
|
|
}
|
|
|
|
func (f *fakeCreateSvc) CreateKnowledgeBase(_ context.Context, kb *sdk.KnowledgeBase) (*sdk.KnowledgeBase, error) {
|
|
f.got = kb
|
|
return f.resp, f.err
|
|
}
|
|
|
|
func TestCreate_Success_Human(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{resp: &sdk.KnowledgeBase{
|
|
ID: "kb_new",
|
|
Name: "Marketing",
|
|
Description: "team docs",
|
|
EmbeddingModelID: "model_x",
|
|
}}
|
|
opts := &CreateOptions{
|
|
Name: "Marketing",
|
|
Description: "team docs",
|
|
EmbeddingModel: "model_x",
|
|
}
|
|
require.NoError(t, runCreate(context.Background(), opts, svc))
|
|
|
|
// Body sent to SDK matches flags.
|
|
require.NotNil(t, svc.got)
|
|
assert.Equal(t, "Marketing", svc.got.Name)
|
|
assert.Equal(t, "team docs", svc.got.Description)
|
|
assert.Equal(t, "model_x", svc.got.EmbeddingModelID)
|
|
|
|
got := out.String()
|
|
for _, want := range []string{"✓", "Created", "Marketing", "kb_new"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("output missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreate_Success_OmitsEmbeddingModelWhenEmpty(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{resp: &sdk.KnowledgeBase{ID: "kb_x", Name: "n"}}
|
|
opts := &CreateOptions{Name: "n"}
|
|
require.NoError(t, runCreate(context.Background(), opts, svc))
|
|
|
|
require.NotNil(t, svc.got)
|
|
assert.Equal(t, "", svc.got.EmbeddingModelID, "embedding-model unset ⇒ empty in request")
|
|
}
|
|
|
|
func TestCreate_NameRequired(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{}
|
|
err := runCreate(context.Background(), &CreateOptions{}, svc)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
assert.Nil(t, svc.got, "service must not be called when name is missing")
|
|
}
|
|
|
|
func TestCreate_NameWhitespaceOnly(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{}
|
|
err := runCreate(context.Background(), &CreateOptions{Name: " "}, svc)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
}
|
|
|
|
func TestCreate_HTTPError_500(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{err: errors.New("HTTP error 500: internal")}
|
|
err := runCreate(context.Background(), &CreateOptions{Name: "x"}, svc)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeServerError, typed.Code)
|
|
}
|
|
|
|
func TestCreate_HTTPError_409Conflict(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{err: errors.New("HTTP error 409: name exists")}
|
|
err := runCreate(context.Background(), &CreateOptions{Name: "dup"}, svc)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceAlreadyExists, typed.Code)
|
|
}
|
|
|
|
func TestCreate_JSONOutput(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeCreateSvc{resp: &sdk.KnowledgeBase{ID: "kb_99", Name: "Eng"}}
|
|
opts := &CreateOptions{Name: "Eng", JSONOut: true}
|
|
require.NoError(t, runCreate(context.Background(), opts, svc))
|
|
|
|
got := out.String()
|
|
assert.True(t, strings.HasPrefix(got, `{"ok":true`), "envelope should start with ok:true; got %q", got)
|
|
assert.Contains(t, got, `"id":"kb_99"`)
|
|
assert.Contains(t, got, `"name":"Eng"`)
|
|
assert.Contains(t, got, `"kb_id":"kb_99"`, "_meta.kb_id should carry the new id")
|
|
}
|