19afd5eed9
v0.2 grounds the CLI's resource commands (kb / doc / chat / query) in a
per-project link file (.weknora/project.yaml) so users don't have to pass
--kb-id on every invocation. Mirrors npm/cargo/git: walk up the cwd tree
to find the project root, override via flag or env when needed.
This commit ships the foundation layer:
- cli/internal/projectlink/ Discover (walk-up, depth=64) / Load / Save
- cmdutil.Factory.ResolveKB 5-level fallback chain:
--kb-id flag → --kb name (ListKnowledgeBases lookup) →
WEKNORA_KB_ID env → walk-up project link → CodeKBIDRequired
- cmdutil.ResolveKBNameToID shared name→id helper used by init / link
/ Factory.ResolveKB (was duplicated 3 ways
in early implementation; reuse review #2)
- cli/cmd/init/ interactive (huh prompt) or flag-driven
first-time setup; refuses to overwrite
without --force
- cli/cmd/link/ non-interactive update; --kb-id and --kb
are mutually exclusive and one is required
Also registers the v0.2 ErrorCode set (all codes for the eight new
commands) and AST-scan identToErrorCode mapping in one place — keeps the
acceptance/contract suite green across the v0.2 commit chain even before
later commits reference each code.
163 lines
5.0 KiB
Go
163 lines
5.0 KiB
Go
package initcmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"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/config"
|
|
"github.com/Tencent/WeKnora/cli/internal/iostreams"
|
|
"github.com/Tencent/WeKnora/cli/internal/projectlink"
|
|
sdk "github.com/Tencent/WeKnora/client"
|
|
)
|
|
|
|
// chdir switches cwd to dir for the duration of the test.
|
|
func chdir(t *testing.T, dir string) {
|
|
t.Helper()
|
|
prev, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.Chdir(dir))
|
|
t.Cleanup(func() { _ = os.Chdir(prev) })
|
|
}
|
|
|
|
// fakeKBServer returns an httptest server answering ListKnowledgeBases.
|
|
func fakeKBServer(t *testing.T, kbs []sdk.KnowledgeBase) *httptest.Server {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/knowledge-bases", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(sdk.KnowledgeBaseListResponse{Success: true, Data: kbs})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
// newFactory builds a minimal Factory for init tests with the supplied
|
|
// CurrentContext + Client closure. Tests inject the cobra-level deps directly
|
|
// because runInit is the test boundary (matches kb/list_test.go pattern).
|
|
func newFactory(currentCtx string, client *sdk.Client) *cmdutil.Factory {
|
|
cfg := &config.Config{
|
|
CurrentContext: currentCtx,
|
|
Contexts: map[string]config.Context{
|
|
currentCtx: {Host: "https://example"},
|
|
},
|
|
}
|
|
return &cmdutil.Factory{
|
|
Config: func() (*config.Config, error) { return cfg, nil },
|
|
Client: func() (*sdk.Client, error) {
|
|
if client == nil {
|
|
return nil, errors.New("client not configured")
|
|
}
|
|
return client, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestInit_CreatesWithFlags_Yes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
chdir(t, dir)
|
|
out, _ := iostreams.SetForTest(t)
|
|
|
|
f := newFactory("default", nil)
|
|
opts := &Options{Context: "default", KBID: "kb_explicit", Yes: true}
|
|
require.NoError(t, runInit(context.Background(), opts, f))
|
|
|
|
// Verify file written with the right contents.
|
|
linkPath := filepath.Join(dir, ".weknora", "project.yaml")
|
|
p, err := projectlink.Load(linkPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "kb_explicit", p.KBID)
|
|
assert.Equal(t, "default", p.Context)
|
|
assert.False(t, p.CreatedAt.IsZero(), "CreatedAt should be stamped")
|
|
|
|
// Human output mentions the path + ✓.
|
|
assert.Contains(t, out.String(), "✓")
|
|
assert.Contains(t, out.String(), "kb_explicit")
|
|
}
|
|
|
|
func TestInit_CreatesWithFlags_KBName(t *testing.T) {
|
|
dir := t.TempDir()
|
|
chdir(t, dir)
|
|
_, _ = iostreams.SetForTest(t)
|
|
|
|
srv := fakeKBServer(t, []sdk.KnowledgeBase{
|
|
{ID: "kb_a", Name: "engineering"},
|
|
{ID: "kb_b", Name: "marketing"},
|
|
})
|
|
cli := sdk.NewClient(srv.URL)
|
|
f := newFactory("default", cli)
|
|
opts := &Options{Context: "default", KBName: "engineering", Yes: true}
|
|
require.NoError(t, runInit(context.Background(), opts, f))
|
|
|
|
linkPath := filepath.Join(dir, ".weknora", "project.yaml")
|
|
p, err := projectlink.Load(linkPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "kb_a", p.KBID, "KB name 'engineering' should resolve to kb_a")
|
|
}
|
|
|
|
func TestInit_RefusesWhenExists(t *testing.T) {
|
|
dir := t.TempDir()
|
|
chdir(t, dir)
|
|
_, _ = iostreams.SetForTest(t)
|
|
|
|
// Pre-seed an existing link.
|
|
linkPath := filepath.Join(dir, ".weknora", "project.yaml")
|
|
require.NoError(t, projectlink.Save(linkPath, &projectlink.Project{KBID: "kb_pre"}))
|
|
|
|
f := newFactory("default", nil)
|
|
opts := &Options{Context: "default", KBID: "kb_new", Yes: true} // no --force
|
|
err := runInit(context.Background(), opts, f)
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeProjectAlreadyLinked, typed.Code)
|
|
|
|
// Original kb_pre must NOT be overwritten.
|
|
p, err := projectlink.Load(linkPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "kb_pre", p.KBID)
|
|
}
|
|
|
|
func TestInit_ForceOverwrites(t *testing.T) {
|
|
dir := t.TempDir()
|
|
chdir(t, dir)
|
|
_, _ = iostreams.SetForTest(t)
|
|
|
|
linkPath := filepath.Join(dir, ".weknora", "project.yaml")
|
|
require.NoError(t, projectlink.Save(linkPath, &projectlink.Project{KBID: "kb_pre"}))
|
|
|
|
f := newFactory("default", nil)
|
|
opts := &Options{Context: "default", KBID: "kb_new", Yes: true, Force: true}
|
|
require.NoError(t, runInit(context.Background(), opts, f))
|
|
|
|
p, err := projectlink.Load(linkPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "kb_new", p.KBID, "--force must overwrite the prior link")
|
|
}
|
|
|
|
func TestInit_JSONOutput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
chdir(t, dir)
|
|
out, _ := iostreams.SetForTest(t)
|
|
|
|
f := newFactory("default", nil)
|
|
opts := &Options{Context: "default", KBID: "kb_explicit", Yes: true, JSONOut: true}
|
|
require.NoError(t, runInit(context.Background(), opts, f))
|
|
|
|
got := out.String()
|
|
assert.True(t, strings.HasPrefix(got, `{"ok":true`), "envelope should start with ok:true; got %q", got)
|
|
assert.Contains(t, got, `"kb_id":"kb_explicit"`)
|
|
assert.Contains(t, got, `"project_link_path"`)
|
|
}
|