Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions cli/cmd/doc/delete_jq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package doc

import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/prompt"
sdk "github.com/Tencent/WeKnora/client"
)

func TestDocDelete_BatchJQProjectsActualVerdict(t *testing.T) {
for _, mode := range []string{"json", "ndjson"} {
for _, failures := range []int{0, 1, 2} {
t.Run(mode+"/"+string(rune('0'+failures))+" failures", func(t *testing.T) {
out, _ := iostreams.SetForTest(t)
var calls []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete || !strings.HasPrefix(r.URL.Path, "/api/v1/knowledge/") {
http.NotFound(w, r)
return
}
calls = append(calls, r.URL.Path)
w.Header().Set("Content-Type", "application/json")
if len(calls) <= failures {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"success":false,"message":"missing"}`))
return
}
_, _ = w.Write([]byte(`{"success":true}`))
}))
t.Cleanup(server.Close)
f := &cmdutil.Factory{
Client: func() (*sdk.Client, error) { return sdk.NewClient(server.URL), nil },
Prompter: func() prompt.Prompter { return prompt.AgentPrompter{} },
}
root := withRootHarnessDoc(NewCmdDelete(f), "doc-a", "doc-b", "-y", "--format", mode,
"--jq", "{ok,status,ids:[.data[].id],successes:.meta.successes,failures:.meta.failures}")
err := root.Execute()
if failures == 0 && err != nil {
t.Fatal(err)
}
if failures > 0 {
var typed *cmdutil.Error
if !errors.As(err, &typed) || typed.Code != cmdutil.CodeOperationFailed {
t.Fatalf("aggregate error=%v, want operation.failed", err)
}
}
var result struct {
OK bool `json:"ok"`
Status string `json:"status"`
IDs []string `json:"ids"`
Successes int `json:"successes"`
Failures int `json:"failures"`
}
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
t.Fatal(err)
}
wantStatus := []string{"success", "partial", "error"}[failures]
if result.OK != (failures == 0) || result.Status != wantStatus ||
result.Successes != 2-failures || result.Failures != failures {
t.Errorf("projected verdict=%+v, want status=%s successes=%d failures=%d",
result, wantStatus, 2-failures, failures)
}
if strings.Join(result.IDs, ",") != "doc-a,doc-b" || len(calls) != 2 {
t.Errorf("IDs=%v requests=%v, want both documents in argv order", result.IDs, calls)
}
})
}
}
}
3 changes: 1 addition & 2 deletions cli/cmd/doc/upload_recursive.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/output"
)

// uploadedFile holds the server-side result for a successful per-file upload.
Expand Down Expand Up @@ -73,7 +72,7 @@ func runUploadRecursive(ctx context.Context, opts *UploadOptions, fopts *cmdutil
if len(matches) == 0 {
if fopts.WantsJSON() {
// Empty batch: emit an all-success envelope with zero items.
return output.WriteBatchEnvelope(iostreams.IO.Out, nil, fopts.TTY, cmdutil.GetProfile())
return cmdutil.EmitBatch(nil, fopts, iostreams.IO.Out, nil)
}
fmt.Fprintf(iostreams.IO.Out, "(no files matched %q under %s)\n", opts.Glob, dir)
return nil
Expand Down
60 changes: 60 additions & 0 deletions cli/cmd/doc/upload_recursive_jq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package doc

import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"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"
)

func TestUploadRecursive_EmptyBatchHonorsJQ(t *testing.T) {
for _, mode := range []string{"json", "ndjson"} {
for _, filter := range []string{"", "{empty:(.data|length),count:.meta.count,status,ok}", "["} {
t.Run(mode+"/"+filter, func(t *testing.T) {
out, _ := iostreams.SetForTest(t)
dir := t.TempDir()
mkTree(t, dir, "only.txt")
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requests.Add(1)
http.Error(w, "unexpected request", http.StatusInternalServerError)
}))
t.Cleanup(server.Close)
f := &cmdutil.Factory{Client: func() (*sdk.Client, error) { return sdk.NewClient(server.URL), nil }}
args := []string{dir, "--recursive", "--glob", "*.pdf", "--kb", "00000000-0000-0000-0000-000000000001", "--format", mode}
if filter != "" {
args = append(args, "--jq", filter)
}
err := withRootHarnessDoc(NewCmdUpload(f), args...).Execute()
assert.Zero(t, requests.Load(), "no match must make no SDK request")
if filter == "[" {
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputInvalidArgument, typed.Code)
assert.Equal(t, 5, cmdutil.ExitCode(err))
assert.Empty(t, out.String())
return
}
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out.Bytes(), &result))
if filter != "" {
assert.Equal(t, map[string]any{"empty": float64(0), "count": float64(0), "status": "success", "ok": true}, result)
} else {
assert.Equal(t, true, result["ok"])
assert.Equal(t, "success", result["status"])
assert.Empty(t, result["data"])
assert.Equal(t, map[string]any{"count": float64(0), "successes": float64(0), "failures": float64(0)}, result["meta"])
}
})
}
}
}
4 changes: 4 additions & 0 deletions cli/internal/cmdutil/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"io"
"time"

"github.com/Tencent/WeKnora/cli/internal/format"
"github.com/Tencent/WeKnora/cli/internal/output"
)

Expand Down Expand Up @@ -94,6 +95,9 @@ func EmitBatch(outcomes []BatchOutcome, fopts *FormatOptions, w io.Writer, resul
items[i].Result = resultFn(o.ID)
}
}
if fopts.JQ != "" {
return mapJQError(format.WriteJSONFiltered(w, output.NewBatchEnvelope(items, globalProfile), nil, fopts.JQ))
}
Comment thread
rudycelekli marked this conversation as resolved.
return output.WriteBatchEnvelope(w, items, fopts.TTY, globalProfile)
}
for _, o := range outcomes {
Expand Down
8 changes: 6 additions & 2 deletions cli/internal/output/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ type BatchItem struct {
// (data array, not error envelope) so agents can iterate detail per id.
// profile is the resolved profile name for the invocation; empty string omits the field.
func WriteBatchEnvelope(w io.Writer, items []BatchItem, indent bool, profile string) error {
return writeJSON(w, NewBatchEnvelope(items, profile), indent)
}

// NewBatchEnvelope builds the canonical batch verdict and per-item counts.
func NewBatchEnvelope(items []BatchItem, profile string) Envelope {
if items == nil {
items = []BatchItem{}
}
Expand All @@ -29,7 +34,7 @@ func WriteBatchEnvelope(w io.Writer, items []BatchItem, indent bool, profile str
failures++
}
}
env := Envelope{
return Envelope{
OK: failures == 0,
Status: batchStatus(successes, failures),
Data: items,
Expand All @@ -40,7 +45,6 @@ func WriteBatchEnvelope(w io.Writer, items []BatchItem, indent bool, profile str
},
Profile: profile,
}
return writeJSON(w, env, indent)
}

// batchStatus maps the (successes, failures) counts to the tri-state envelope
Expand Down