5b07c9ab87
New subtree (chunk list / view / delete) exposes RAG retrieval debugging primitives with SDK-grounded field set (23 Chunk fields). Pagination follows v0.4 canon: --limit / --page-size (1..1000) / --all-pages. - chunk list --doc <id>: enumerate by ChunkIndex (separate from search chunks which is hybrid retrieval; Long help documents the distinction) - chunk view <id>: scope-less render via /chunks/by-id route; full content verbatim - chunk delete <id> --doc <id>: scope-flag + scope-id; L-13 destructive; 404 NOT idempotent; resource.not_found / auth.forbidden / input.confirmation_required typed exit codes documented in Long help MCP server gains chunk_list as 10th curated tool. Schema deliberately exposes only doc_id + limit (no pagination workflow on MCP); response includes truncated_at_limit flag when total > limit. cli/AGENTS.md MCP curation rationale rewritten: curated read-only is a deliberate product call because the server side does not yet enforce per-token scope. When server scope ships, mutation tools can land in the MCP surface. Shared helper cli/internal/text/timeago_string.go (FuzzyAgoStr) extracted from session list during the C2 quality-review pass.
107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
package chunkcmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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"
|
|
"github.com/Tencent/WeKnora/cli/internal/testutil"
|
|
)
|
|
|
|
type fakeChunkDeleteSvc struct {
|
|
gotDocID, gotChunkID string
|
|
err error
|
|
}
|
|
|
|
func (f *fakeChunkDeleteSvc) DeleteChunk(_ context.Context, docID, chunkID string) error {
|
|
f.gotDocID = docID
|
|
f.gotChunkID = chunkID
|
|
return f.err
|
|
}
|
|
|
|
func TestDelete_NonTTY_NoYes_ExitTen(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeChunkDeleteSvc{}
|
|
err := runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "c1", DocID: "doc_abc", Yes: false},
|
|
&cmdutil.JSONOptions{}, svc, &testutil.ConfirmPrompter{})
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeInputConfirmationRequired, typed.Code)
|
|
assert.Empty(t, svc.gotChunkID, "must not call DeleteChunk without confirm")
|
|
assert.Equal(t, 10, cmdutil.ExitCode(err), "exit 10 per destructive-write protocol")
|
|
}
|
|
|
|
func TestDelete_WithYes_PassesBothIDs(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeChunkDeleteSvc{}
|
|
require.NoError(t, runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "c1", DocID: "doc_abc", Yes: true},
|
|
&cmdutil.JSONOptions{}, svc, &testutil.ConfirmPrompter{}))
|
|
assert.Equal(t, "doc_abc", svc.gotDocID)
|
|
assert.Equal(t, "c1", svc.gotChunkID)
|
|
}
|
|
|
|
func TestDelete_MissingDoc_FlagError(t *testing.T) {
|
|
cmd := NewCmdDelete(nil)
|
|
cmd.SetArgs([]string{"c1"}) // no --doc
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
require.Error(t, cmd.Execute())
|
|
}
|
|
|
|
func TestDelete_404_PropagatesNotFound(t *testing.T) {
|
|
_, _ = iostreams.SetForTest(t)
|
|
svc := &fakeChunkDeleteSvc{err: errors.New("HTTP error 404: not found")}
|
|
err := runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "missing", DocID: "doc_abc", Yes: true},
|
|
&cmdutil.JSONOptions{}, svc, &testutil.ConfirmPrompter{})
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeResourceNotFound, typed.Code)
|
|
}
|
|
|
|
func TestDelete_TTY_ConfirmYes_Calls(t *testing.T) {
|
|
_, _ = iostreams.SetForTestWithTTY(t)
|
|
svc := &fakeChunkDeleteSvc{}
|
|
p := &testutil.ConfirmPrompter{Answer: true}
|
|
require.NoError(t, runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "c1", DocID: "doc_abc"},
|
|
nil, svc, p))
|
|
assert.True(t, p.Asked)
|
|
assert.Equal(t, "c1", svc.gotChunkID)
|
|
}
|
|
|
|
func TestDelete_TTY_ConfirmNo_Aborts(t *testing.T) {
|
|
_, errBuf := iostreams.SetForTestWithTTY(t)
|
|
svc := &fakeChunkDeleteSvc{}
|
|
p := &testutil.ConfirmPrompter{Answer: false}
|
|
err := runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "c1", DocID: "doc_abc"},
|
|
nil, svc, p)
|
|
require.Error(t, err)
|
|
var typed *cmdutil.Error
|
|
require.ErrorAs(t, err, &typed)
|
|
assert.Equal(t, cmdutil.CodeUserAborted, typed.Code)
|
|
assert.Empty(t, svc.gotChunkID, "answer=no must not call DeleteChunk")
|
|
assert.Contains(t, errBuf.String(), "Aborted")
|
|
}
|
|
|
|
func TestDelete_JSON_BareObject(t *testing.T) {
|
|
out, _ := iostreams.SetForTest(t)
|
|
svc := &fakeChunkDeleteSvc{}
|
|
require.NoError(t, runDelete(context.Background(),
|
|
&DeleteOptions{ChunkID: "c1", DocID: "doc_abc", Yes: true},
|
|
&cmdutil.JSONOptions{}, svc, &testutil.ConfirmPrompter{}))
|
|
body := out.String()
|
|
assert.Contains(t, body, `"id":"c1"`)
|
|
assert.Contains(t, body, `"deleted":true`)
|
|
}
|