c87e35b34b
Code-reuse polish (post-implementation review pass): - Extract text.OneLine(maxWidth, s) helper combining preview-row normalization (newline/CR/tab → space) with text.Truncate's UTF-8-safe truncation. Replaces agent/view.go truncate1Line (ASCII '...' + byte-slice CJK-unsafe) and chunk/list.go singleLine. - Lift cmdutil.OpenInput(path) for the '-' = stdin / else os.Open pattern shared across agent create/edit and the api command. Replaces agent/create.go's private openInput. - Strip inline doc-spec parentheticals from source comments — those belong in commit messages and project docs, not in source where they rot. Pre-PR audit fixes: - doc upload: reject `--metadata` paired with `--from-url` as input.invalid_argument up-front (the URL-ingest request type has no metadata field server-side, so the pair would otherwise silently drop). Long help and CHANGELOG updated to call out the asymmetry. - doc upload (file path): map sdk.ErrDuplicateFile sentinel to resource.already_exists. The sentinel arrives with no "HTTP error <n>:" prefix because the SDK short-circuits on file-hash before reading the HTTP status, so the previous WrapHTTP fall-through misclassified it as network.error with a misleading "check base URL reachability" hint. The --from-url branch already handled ErrDuplicateURL this way; this closes the asymmetry. Caught by e2e re-upload of an already-ingested file; regression test added. - README exit-10 enumeration adds `agent delete` and `chunk delete` (these were missing alongside the v0.5 destructive verbs they were meant to gate). Docs sync: - cli/README.md: command tree now includes the chunk subtree; adds agent / chunk lines to the 5-minute quickstart; adds a "Contributing / Reporting issues" section pointing at the repo's SECURITY.md and AGENTS.md; drops third-party CLI parallels from the surface description. - cli/AGENTS.md: "Command surface design SOP" gains the flag-vs-escape-hatch step. "CRUD command flag canon" renamed to the hard-required-flags pattern with the contrast (TTY-prompts-fill) defined inline rather than via opaque shorthand. - cli/CHANGELOG.md: search docs case-sensitivity shift promoted to its own #### Breaking changes subsection. MCP doc_list filter count corrected from 5 to 6. Drops the bogus go.mod yaml.v3 entry (yaml.v3 was already a dependency on main; v0.5 added zero go.mod lines). Replaces internal-Go identifiers (fuzzyTime, NoOptDefVal) with user-language and drops the § section-symbol jargon.
498 lines
18 KiB
Go
498 lines
18 KiB
Go
package doc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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/iostreams"
|
|
sdk "github.com/Tencent/WeKnora/client"
|
|
)
|
|
|
|
// fakeUploadSvc captures call arguments and returns canned responses.
|
|
type fakeUploadSvc struct {
|
|
resp *sdk.Knowledge
|
|
err error
|
|
urlResp *sdk.Knowledge
|
|
urlErr error
|
|
got struct {
|
|
kbID, filePath, customName, channel string
|
|
metadata map[string]string
|
|
enableMultimodel *bool
|
|
urlReq sdk.CreateKnowledgeFromURLRequest
|
|
}
|
|
}
|
|
|
|
func (f *fakeUploadSvc) CreateKnowledgeFromFile(
|
|
_ context.Context,
|
|
kbID, filePath string,
|
|
metadata map[string]string,
|
|
enableMultimodel *bool,
|
|
customFileName, channel string,
|
|
) (*sdk.Knowledge, error) {
|
|
f.got.kbID = kbID
|
|
f.got.filePath = filePath
|
|
f.got.metadata = metadata
|
|
f.got.enableMultimodel = enableMultimodel
|
|
f.got.customName = customFileName
|
|
f.got.channel = channel
|
|
return f.resp, f.err
|
|
}
|
|
|
|
func (f *fakeUploadSvc) CreateKnowledgeFromURL(
|
|
_ context.Context,
|
|
kbID string,
|
|
req sdk.CreateKnowledgeFromURLRequest,
|
|
) (*sdk.Knowledge, error) {
|
|
f.got.kbID = kbID
|
|
f.got.urlReq = req
|
|
return f.urlResp, f.urlErr
|
|
}
|
|
|
|
// writeTempFile creates a regular file under t.TempDir() with sample content.
|
|
func writeTempFile(t *testing.T, name string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), name)
|
|
require.NoError(t, os.WriteFile(path, []byte("hello world"), 0o644))
|
|
return path
|
|
}
|
|
|
|
func TestUpload_Success_Human(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "report.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_99", FileName: "report.pdf"}}
|
|
opts := &UploadOptions{}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
|
|
assert.Equal(t, "kb_xxx", svc.got.kbID)
|
|
assert.Equal(t, path, svc.got.filePath)
|
|
assert.Equal(t, "", svc.got.customName, "no --name ⇒ empty (server uses base name)")
|
|
assert.Equal(t, uploadChannel, svc.got.channel)
|
|
assert.Nil(t, svc.got.metadata)
|
|
assert.Nil(t, svc.got.enableMultimodel)
|
|
|
|
got := out.String()
|
|
for _, want := range []string{"✓", "Uploaded", "report.pdf", "doc_99"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("output missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpload_Success_CustomName(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "q3.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_88", FileName: "q3.pdf"}}
|
|
opts := &UploadOptions{Name: "Q3 Marketing Report.pdf"}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, "Q3 Marketing Report.pdf", svc.got.customName)
|
|
}
|
|
|
|
func TestUpload_Success_JSON(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "a.md")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_77", FileName: "a.md"}}
|
|
opts := &UploadOptions{}
|
|
require.NoError(t, runUpload(context.Background(), opts, &cmdutil.JSONOptions{}, svc, "kb_xxx", path))
|
|
|
|
got := out.String()
|
|
assert.True(t, strings.HasPrefix(strings.TrimSpace(got), `{"id":"doc_77"`), "expected bare Knowledge object; got %q", got)
|
|
assert.Contains(t, got, `"file_name":"a.md"`)
|
|
assert.NotContains(t, got, `"ok":`)
|
|
}
|
|
|
|
func TestUpload_HTTPError_500(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "x.txt")
|
|
svc := &fakeUploadSvc{err: errors.New("HTTP error 500: internal")}
|
|
err := runUpload(context.Background(), &UploadOptions{}, nil, svc, "kb_xxx", path)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeServerError, typed.Code)
|
|
}
|
|
|
|
func TestUpload_HTTPError_409Conflict(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "dup.pdf")
|
|
svc := &fakeUploadSvc{err: errors.New("HTTP error 409: file exists")}
|
|
err := runUpload(context.Background(), &UploadOptions{}, nil, svc, "kb_xxx", path)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceAlreadyExists, typed.Code)
|
|
}
|
|
|
|
// TestUpload_DuplicateFileMaps_resource_already_exists pins the contract that
|
|
// the SDK's sentinel sdk.ErrDuplicateFile (returned with no "HTTP error <n>:"
|
|
// prefix because the duplicate is detected by file-hash short-circuit, not by
|
|
// status code) is mapped to resource.already_exists. Prior regression: the
|
|
// file-upload path forwarded the sentinel to WrapHTTP, which classified the
|
|
// prefix-less message as network.error — symmetric with the --from-url branch
|
|
// which already handled ErrDuplicateURL.
|
|
func TestUpload_DuplicateFileMaps_resource_already_exists(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "dup.md")
|
|
svc := &fakeUploadSvc{err: sdk.ErrDuplicateFile}
|
|
err := runUpload(context.Background(), &UploadOptions{}, nil, svc, "kb_xxx", path)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceAlreadyExists, typed.Code)
|
|
}
|
|
|
|
func TestValidateUploadPath_NotFound(t *testing.T) {
|
|
missing := filepath.Join(t.TempDir(), "does-not-exist.pdf")
|
|
err := validateUploadPath(missing)
|
|
require.Error(t, err)
|
|
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeUploadFileNotFound, typed.Code)
|
|
}
|
|
|
|
func TestValidateUploadPath_DirectoryRejected(t *testing.T) {
|
|
dir := t.TempDir() // already exists, is a dir
|
|
err := validateUploadPath(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, "not a regular file")
|
|
}
|
|
|
|
func TestValidateUploadPath_RegularFileAccepted(t *testing.T) {
|
|
path := writeTempFile(t, "ok.txt")
|
|
require.NoError(t, validateUploadPath(path))
|
|
}
|
|
|
|
func TestValidateUploadPath_SymlinkToFileAccepted(t *testing.T) {
|
|
target := writeTempFile(t, "target.txt")
|
|
link := filepath.Join(t.TempDir(), "link.txt")
|
|
if err := os.Symlink(target, link); err != nil {
|
|
t.Skipf("symlink not supported on this platform: %v", err)
|
|
}
|
|
// os.Stat (not Lstat) should follow the symlink and report regular file.
|
|
require.NoError(t, validateUploadPath(link))
|
|
}
|
|
|
|
// --from-url tests (4-N1).
|
|
|
|
func TestUploadFromURL_Success_Human(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_url_1", FileName: "whitepaper.pdf"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/whitepaper.pdf"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
|
|
assert.Equal(t, "kb_xxx", svc.got.kbID)
|
|
assert.Equal(t, "https://example.com/whitepaper.pdf", svc.got.urlReq.URL)
|
|
assert.Equal(t, "api", svc.got.urlReq.Channel)
|
|
assert.Contains(t, out.String(), "Ingested")
|
|
assert.Contains(t, out.String(), "doc_url_1")
|
|
}
|
|
|
|
func TestUploadFromURL_WithName_Passes_AsFileName(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_url_2"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/article.html", Name: "Q3 Article"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
assert.Equal(t, "Q3 Article", svc.got.urlReq.FileName,
|
|
"--name must be forwarded as FileName (server uses it for file-vs-crawl mode hint)")
|
|
}
|
|
|
|
func TestUploadFromURL_JSON_BareObject(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_url_3", FileName: "ok.pdf"}}
|
|
jopts := &cmdutil.JSONOptions{}
|
|
require.NoError(t, runUploadFromURL(context.Background(),
|
|
&UploadOptions{FromURL: "https://example.com/ok.pdf"}, jopts, svc, "kb_xxx"))
|
|
got := out.String()
|
|
assert.Contains(t, got, `"id":"doc_url_3"`)
|
|
assert.NotContains(t, got, `"ok":`)
|
|
assert.NotContains(t, got, `"risk":`)
|
|
}
|
|
|
|
func TestUploadFromURL_DuplicateURLMaps_resource_already_exists(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{
|
|
urlResp: &sdk.Knowledge{ID: "doc_existing"},
|
|
urlErr: sdk.ErrDuplicateURL,
|
|
}
|
|
err := runUploadFromURL(context.Background(),
|
|
&UploadOptions{FromURL: "https://example.com/dup.pdf"}, nil, svc, "kb_xxx")
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceAlreadyExists, typed.Code)
|
|
}
|
|
|
|
func TestValidateUploadFlags_FromURL_OK(t *testing.T) {
|
|
require.NoError(t, validateUploadFlags(&UploadOptions{FromURL: "https://example.com/x.pdf"}, nil))
|
|
}
|
|
|
|
func TestValidateUploadFlags_FromURL_WithPositional_Rejected(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{FromURL: "https://example.com/x.pdf"}, []string{"/tmp/x.pdf"})
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
}
|
|
|
|
func TestValidateUploadFlags_FromURL_WithRecursive_Rejected(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{FromURL: "https://example.com/x.pdf", Recursive: true}, nil)
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
}
|
|
|
|
// The server's URL-ingest request type has no Metadata field; the CLI must
|
|
// reject --metadata + --from-url upfront so callers don't think they've set
|
|
// metadata that the server then silently drops on the wire.
|
|
func TestValidateUploadFlags_FromURL_WithMetadata_Rejected(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{
|
|
FromURL: "https://example.com/x.pdf",
|
|
Metadata: []string{"team=alpha"},
|
|
}, nil)
|
|
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, "--metadata is not supported with --from-url")
|
|
}
|
|
|
|
func TestValidateUploadFlags_FromURL_BadScheme(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{FromURL: "file:///etc/passwd"}, nil)
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
}
|
|
|
|
func TestValidateUploadFlags_FromURL_NoHost(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{FromURL: "https://"}, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestValidateUploadFlags_NoPathOrURL_Rejected(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{}, nil)
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
|
|
}
|
|
|
|
// --- C10 expanded flags: multimodel / metadata / channel / URL-mode extras ---
|
|
|
|
func TestUpload_EnableMultimodel_Set_True(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "mm.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_mm", FileName: "mm.pdf"}}
|
|
mm := true
|
|
opts := &UploadOptions{EnableMultimodel: &mm}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
require.NotNil(t, svc.got.enableMultimodel, "expected non-nil *bool when flag set")
|
|
assert.True(t, *svc.got.enableMultimodel)
|
|
}
|
|
|
|
func TestUpload_EnableMultimodel_Set_False(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "mm.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_mm", FileName: "mm.pdf"}}
|
|
mm := false
|
|
opts := &UploadOptions{EnableMultimodel: &mm}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
require.NotNil(t, svc.got.enableMultimodel, "explicit false must still surface as non-nil *bool")
|
|
assert.False(t, *svc.got.enableMultimodel)
|
|
}
|
|
|
|
// TestParseTriBool pins the empty-string-rejects behavior. Bare
|
|
// --enable-multimodel maps to "true" via NoOptDefVal before the flag reaches
|
|
// parseTriBool, so an empty value here always indicates an explicit
|
|
// --enable-multimodel="" (e.g. uninterpolated $VAR). Silently coercing
|
|
// empty to true used to surprise users.
|
|
func TestParseTriBool(t *testing.T) {
|
|
for _, c := range []struct {
|
|
in string
|
|
want bool
|
|
wantErr bool
|
|
}{
|
|
{"true", true, false},
|
|
{"1", true, false},
|
|
{"yes", true, false},
|
|
{"false", false, false},
|
|
{"0", false, false},
|
|
{"no", false, false},
|
|
{"", false, true}, // explicit empty rejected
|
|
{" ", false, true}, // whitespace rejected
|
|
{"maybe", false, true},
|
|
} {
|
|
t.Run(c.in, func(t *testing.T) {
|
|
got, err := parseTriBool(c.in)
|
|
if c.wantErr {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "input.invalid_argument")
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, c.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpload_Metadata_ParseKV(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "m.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_m", FileName: "m.pdf"}}
|
|
opts := &UploadOptions{Metadata: []string{"foo=bar", "baz=qux"}}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, map[string]string{"foo": "bar", "baz": "qux"}, svc.got.metadata)
|
|
}
|
|
|
|
func TestUpload_Metadata_EmptyValueAllowed(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "m.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_m", FileName: "m.pdf"}}
|
|
opts := &UploadOptions{Metadata: []string{"foo="}}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, map[string]string{"foo": ""}, svc.got.metadata)
|
|
}
|
|
|
|
func TestUpload_Metadata_LastWins(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "m.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_m", FileName: "m.pdf"}}
|
|
opts := &UploadOptions{Metadata: []string{"k=v1", "k=v2"}}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, map[string]string{"k": "v2"}, svc.got.metadata)
|
|
}
|
|
|
|
func TestUpload_Metadata_InvalidFormat_NoEquals(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "m.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_m", FileName: "m.pdf"}}
|
|
opts := &UploadOptions{Metadata: []string{"foo"}}
|
|
err := runUpload(context.Background(), opts, 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)
|
|
}
|
|
|
|
func TestUpload_Metadata_InvalidFormat_EmptyKey(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "m.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_m", FileName: "m.pdf"}}
|
|
opts := &UploadOptions{Metadata: []string{"=bar"}}
|
|
err := runUpload(context.Background(), opts, 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)
|
|
}
|
|
|
|
func TestUpload_Channel_Override(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "c.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_c", FileName: "c.pdf"}}
|
|
opts := &UploadOptions{Channel: "browser_extension"}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, "browser_extension", svc.got.channel)
|
|
}
|
|
|
|
func TestUpload_Channel_DefaultStillAPI(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
path := writeTempFile(t, "c.pdf")
|
|
svc := &fakeUploadSvc{resp: &sdk.Knowledge{ID: "doc_c", FileName: "c.pdf"}}
|
|
// Empty Channel is the runUpload contract for "use default".
|
|
opts := &UploadOptions{}
|
|
require.NoError(t, runUpload(context.Background(), opts, nil, svc, "kb_xxx", path))
|
|
assert.Equal(t, uploadChannel, svc.got.channel)
|
|
}
|
|
|
|
// URL-mode metadata happy paths
|
|
|
|
func TestUploadFromURL_Title(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_u"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/a.pdf", Title: "My Title"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
assert.Equal(t, "My Title", svc.got.urlReq.Title)
|
|
}
|
|
|
|
func TestUploadFromURL_FileType(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_u"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/no-ext", FileType: "pdf"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
assert.Equal(t, "pdf", svc.got.urlReq.FileType)
|
|
}
|
|
|
|
func TestUploadFromURL_TagID(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_u"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/a.pdf", TagID: "tag_99"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
assert.Equal(t, "tag_99", svc.got.urlReq.TagID)
|
|
}
|
|
|
|
func TestUploadFromURL_EnableMultimodel_Forwarded(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_u"}}
|
|
mm := true
|
|
opts := &UploadOptions{FromURL: "https://example.com/a.pdf", EnableMultimodel: &mm}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
require.NotNil(t, svc.got.urlReq.EnableMultimodel)
|
|
assert.True(t, *svc.got.urlReq.EnableMultimodel)
|
|
}
|
|
|
|
func TestUploadFromURL_Channel_Override(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeUploadSvc{urlResp: &sdk.Knowledge{ID: "doc_u"}}
|
|
opts := &UploadOptions{FromURL: "https://example.com/a.pdf", Channel: "web"}
|
|
require.NoError(t, runUploadFromURL(context.Background(), opts, nil, svc, "kb_xxx"))
|
|
assert.Equal(t, "web", svc.got.urlReq.Channel)
|
|
}
|
|
|
|
// URL-only flag misuse: error when used without --from-url.
|
|
// validateUploadFlags should reject --title/--file-type/--tag-id paired
|
|
// with a positional file path (i.e., no --from-url).
|
|
|
|
func TestValidateUploadFlags_Title_RequiresFromURL(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{Title: "x"}, []string{"/tmp/x.pdf"})
|
|
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, "--title")
|
|
}
|
|
|
|
func TestValidateUploadFlags_FileType_RequiresFromURL(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{FileType: "pdf"}, []string{"/tmp/x.pdf"})
|
|
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, "--file-type")
|
|
}
|
|
|
|
func TestValidateUploadFlags_TagID_RequiresFromURL(t *testing.T) {
|
|
err := validateUploadFlags(&UploadOptions{TagID: "tag_x"}, []string{"/tmp/x.pdf"})
|
|
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, "--tag-id")
|
|
}
|