Files
Enterprise-Knowledge-Base/cli/cmd/init/init_test.go
T
nullkey 19afd5eed9 feat(cli): add project-link foundation with init and link commands
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.
2026-05-12 13:20:42 +08:00

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"`)
}