Files
Enterprise-Knowledge-Base/cli/cmd/doc/upload_recursive_test.go
T
nullkey cc8254f862 refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):

1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
   (gh, aws, stripe, lark): none expose --dry-run. The mainstream that
   does (kubectl/git/helm/ansible) operates on declarative manifests
   or local state where the preview is materially different from the
   executed action. WeKnora's CLI just echoed the same parameters
   that would have gone on the wire — the preview added no real
   signal over `--help` + reading the call site. Removes:
   - root --dry-run persistent flag + cmdutil/dryrun.go
   - DryRun fields + EmitDryRun calls in 12 write commands
   - format.Envelope.DryRun field
   - 8 corresponding *_test.go cases
   - --dry-run mention from README.md and CHANGELOG.md
   - "dry_run":false from 16 golden envelopes

2. Migrate every --json output to bare data:
   - New format.WriteJSON / WriteJSONFiltered helpers
     (cli/internal/format/bare.go) share filterArrayItems /
     filterObjectKeys / writeJQ with the (still-live for now) envelope
     filter helpers.
   - Read commands (kb/doc/session list+view, search chunks/docs/
     sessions/kb, auth list/status, agent list/view, context list,
     doctor) emit bare arrays / objects on stdout.
   - Write commands (kb create/edit/delete/pin/empty, doc upload/
     upload_recursive/delete, session delete, auth login/logout/
     refresh/token, link/unlink, context add/use/remove, agent
     invoke, chat, api, version) emit bare result objects. Risk
     classification dropped — the resource + exit code already
     convey the action.

Per-command shape changes:
   list / search       → []T   (was {ok, data:{items:[…]}})
   view                → T     (was {ok, data:T, _meta:…})
   create / edit       → T
   delete / pin / etc. → {id, …action result…}
   doctor              → {summary, checks}
   api                 → {status, headers, body}

_meta dropped on the read path:
   pagination (page/page_size/total/has_more) — agents iterate with
   --all-pages or accept --limit (gh CLI parity);
   kb_id / context echo — caller already knows what it asked for.

Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
2026-05-15 12:03:56 +08:00

195 lines
6.1 KiB
Go

package doc
import (
"context"
"errors"
"os"
"path/filepath"
"sort"
"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"
)
// scriptedUploadSvc records every CreateKnowledgeFromFile call and returns
// per-path scripted results.
type scriptedUploadSvc struct {
results map[string]struct {
k *sdk.Knowledge
err error
}
called []string
}
func (s *scriptedUploadSvc) CreateKnowledgeFromFile(
_ context.Context,
_, filePath string,
_ map[string]string,
_ *bool,
_, _ string,
) (*sdk.Knowledge, error) {
s.called = append(s.called, filepath.Base(filePath))
r, ok := s.results[filepath.Base(filePath)]
if !ok {
return &sdk.Knowledge{ID: "doc_" + filepath.Base(filePath), FileName: filepath.Base(filePath)}, nil
}
return r.k, r.err
}
// CreateKnowledgeFromURL satisfies UploadService but is unused by the
// recursive-walk path. Recursive upload only goes through CreateKnowledgeFromFile.
func (s *scriptedUploadSvc) CreateKnowledgeFromURL(
_ context.Context,
_ string,
_ sdk.CreateKnowledgeFromURLRequest,
) (*sdk.Knowledge, error) {
return nil, nil
}
func mkTree(t *testing.T, base string, names ...string) {
t.Helper()
for _, n := range names {
full := filepath.Join(base, n)
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
require.NoError(t, os.WriteFile(full, []byte("x"), 0o644))
}
}
func TestUploadRecursive_WalksAllFiles(t *testing.T) {
out, _ := iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "a.pdf", "b.pdf", "sub/c.pdf")
svc := &scriptedUploadSvc{}
opts := &UploadOptions{Recursive: true, Glob: "*"}
require.NoError(t, runUploadRecursive(context.Background(), opts, nil, svc, "kb_xxx", dir))
sort.Strings(svc.called)
assert.Equal(t, []string{"a.pdf", "b.pdf", "c.pdf"}, svc.called)
got := out.String()
for _, w := range []string{"a.pdf", "b.pdf", "c.pdf", "Uploaded 3"} {
assert.Contains(t, got, w)
}
}
func TestUploadRecursive_GlobFilter(t *testing.T) {
_, _ = iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "doc.pdf", "ignore.txt", "sub/keep.pdf", "sub/also-ignore.md")
svc := &scriptedUploadSvc{}
opts := &UploadOptions{Recursive: true, Glob: "*.pdf"}
require.NoError(t, runUploadRecursive(context.Background(), opts, nil, svc, "kb_xxx", dir))
sort.Strings(svc.called)
assert.Equal(t, []string{"doc.pdf", "keep.pdf"}, svc.called)
}
func TestUploadRecursive_PartialFailure_Exits1(t *testing.T) {
out, _ := iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "ok.pdf", "bad.pdf")
svc := &scriptedUploadSvc{results: map[string]struct {
k *sdk.Knowledge
err error
}{
"bad.pdf": {err: errors.New("HTTP error 500: internal")},
}}
opts := &UploadOptions{Recursive: true, Glob: "*"}
err := runUploadRecursive(context.Background(), opts, nil, svc, "kb_xxx", dir)
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
// CodeServerError preserves the 500 classification of the underlying
// SDK error — the recursive wrapper just aggregates.
assert.Equal(t, cmdutil.CodeServerError, typed.Code)
got := out.String()
assert.Contains(t, got, "OK") // ok.pdf still succeeded
assert.Contains(t, got, "FAIL")
assert.Contains(t, got, "Uploaded 1")
assert.Contains(t, got, "Failed 1")
}
func TestUploadRecursive_NoMatches(t *testing.T) {
out, _ := iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "only.txt")
svc := &scriptedUploadSvc{}
opts := &UploadOptions{Recursive: true, Glob: "*.pdf"}
require.NoError(t, runUploadRecursive(context.Background(), opts, nil, svc, "kb_xxx", dir))
assert.Len(t, svc.called, 0)
assert.Contains(t, strings.ToLower(out.String()), "no files matched")
}
func TestUploadRecursive_NotADirectory(t *testing.T) {
_, _ = iostreams.SetForTest(t)
path := writeTempFile(t, "single.pdf")
svc := &scriptedUploadSvc{}
err := runUploadRecursive(context.Background(), &UploadOptions{Recursive: true, Glob: "*"}, nil, svc, "kb_xxx", path)
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
assert.Contains(t, typed.Message, "directory")
}
func TestUploadRecursive_RejectsNameFlag(t *testing.T) {
_, _ = iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "a.pdf")
svc := &scriptedUploadSvc{}
opts := &UploadOptions{Recursive: true, Glob: "*", Name: "single-name.pdf"}
err := runUploadRecursive(context.Background(), opts, nil, svc, "kb_xxx", dir)
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
assert.Contains(t, typed.Message, "--name")
}
func TestUploadRecursive_JSON_BareObject(t *testing.T) {
out, _ := iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "ok.pdf", "bad.pdf")
svc := &scriptedUploadSvc{results: map[string]struct {
k *sdk.Knowledge
err error
}{
"bad.pdf": {err: errors.New("HTTP error 500: internal")},
}}
opts := &UploadOptions{Recursive: true, Glob: "*"}
err := runUploadRecursive(context.Background(), opts, &cmdutil.JSONOptions{}, svc, "kb_xxx", dir)
require.Error(t, err) // partial failure → typed error
body := out.String()
assert.Contains(t, body, `"kb_id":"kb_xxx"`)
assert.Contains(t, body, `"uploaded":`)
assert.Contains(t, body, `"failed":`)
assert.Contains(t, body, `ok.pdf`)
assert.Contains(t, body, `bad.pdf`)
assert.NotContains(t, body, `"ok":`, "bare output must not carry envelope keys")
// --json must emit exactly ONE JSON document. Per-file "FAIL"/"OK"
// progress lines belong on the human path; the typed error is Silent so
// the root handler doesn't write anything additional to stdout.
assert.NotContains(t, body, "FAIL ", "per-file plain lines must not appear under --json")
assert.NotContains(t, body, "OK ", "per-file plain lines must not appear under --json")
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.True(t, typed.Silent, "JSON-path partial failure must be Silent")
assert.Equal(t, cmdutil.CodeServerError, typed.Code)
}