mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Compare commits
1 Commits
v1.0.63
...
feat/docs_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5d7a42d3b |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,6 +27,9 @@ Thumbs.db
|
||||
# Go
|
||||
docs/ref
|
||||
docs/
|
||||
!tests/cli_e2e/docs/
|
||||
!tests/cli_e2e/docs/*.go
|
||||
!tests/cli_e2e/docs/*.md
|
||||
vendor/
|
||||
|
||||
|
||||
|
||||
@@ -20,13 +20,28 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newTestApiCmd(f *cmdutil.Factory, runF func(*APIOptions) error) *cobra.Command {
|
||||
cmd := NewCmdApi(f, runF)
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newTestRootCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "lark-cli",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -54,7 +69,7 @@ func TestApiCmd_DryRun(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--dry-run"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -77,7 +92,7 @@ func TestApiCmd_NullParamsWithPageSize(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--params", "null", "--page-size", "50", "--as", "bot", "--dry-run"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("--params null with --page-size should not error, got: %v", err)
|
||||
@@ -98,7 +113,7 @@ func TestApiCmd_BotMode(t *testing.T) {
|
||||
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{"result": "success"}},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -125,7 +140,7 @@ func TestApiCmd_MissingArgs(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET"}) // missing path
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
@@ -138,7 +153,7 @@ func TestApiCmd_InvalidParamsJSON(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--params", "{bad"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
@@ -151,7 +166,7 @@ func TestApiValidArgsFunction(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
fn := cmd.ValidArgsFunction
|
||||
|
||||
tests := []struct {
|
||||
@@ -217,7 +232,7 @@ func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, SupportedIdentities: 2,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
flag := cmd.Flags().Lookup("as")
|
||||
if flag == nil {
|
||||
t.Fatal("expected --as flag to be registered")
|
||||
@@ -236,7 +251,7 @@ func TestApiCmd_PageLimitDefault(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -255,7 +270,7 @@ func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--params", "-", "--data", "-"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
@@ -272,7 +287,7 @@ func TestApiCmd_OutputAndPageAllConflict(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return apiRun(opts)
|
||||
})
|
||||
@@ -297,7 +312,7 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
|
||||
ContentType: "application/octet-stream",
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/drive/v1/files/xxx/download", "--as", "bot"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -328,7 +343,7 @@ func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users/u123", "--as", "bot", "--page-all", "--format", "ndjson"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -368,7 +383,7 @@ func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/im/v1/chats/oc_xxx/announcement", "--as", "bot", "--page-all"})
|
||||
err := cmd.Execute()
|
||||
// Should return an error
|
||||
@@ -409,7 +424,7 @@ func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--format", "ndjson"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -448,7 +463,7 @@ func TestApiCmd_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--format", "ndjson"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
@@ -483,7 +498,7 @@ func TestApiCmd_PageAll_BatchAPI_DefaultJSONEnvelope(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -549,8 +564,8 @@ func TestApiCmd_PageAll_DefaultJSONRunsContentSafety(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.AddCommand(NewCmdApi(f, nil))
|
||||
root := newTestRootCmd()
|
||||
root.AddCommand(newTestApiCmd(f, nil))
|
||||
root.SetArgs([]string{"api", "GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -600,8 +615,8 @@ func TestApiCmd_PageAll_StreamFormatRunsContentSafety(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.AddCommand(NewCmdApi(f, nil))
|
||||
root := newTestRootCmd()
|
||||
root.AddCommand(newTestApiCmd(f, nil))
|
||||
root.SetArgs([]string{"api", "GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--format", "ndjson"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -656,8 +671,8 @@ func TestApiCmd_PageAll_StreamFormatBlockSkipsBlockedPage(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.AddCommand(NewCmdApi(f, nil))
|
||||
root := newTestRootCmd()
|
||||
root.AddCommand(newTestApiCmd(f, nil))
|
||||
root.SetArgs([]string{"api", "GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--format", "ndjson"})
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
@@ -721,7 +736,7 @@ func TestApiCmd_JqFlag_Parsing(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -741,7 +756,7 @@ func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -760,7 +775,7 @@ func TestApiCmd_JqAndOutputConflict(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", ".data", "--output", "file.bin"})
|
||||
@@ -791,7 +806,7 @@ func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test/jq", "--as", "bot", "--jq", ".data.items[].name"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -812,7 +827,7 @@ func TestApiCmd_JqAndFormatConflict(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", ".data", "--format", "ndjson"})
|
||||
@@ -830,7 +845,7 @@ func TestApiCmd_JqInvalidExpression(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", "invalid["})
|
||||
@@ -859,7 +874,7 @@ func TestApiCmd_PageAll_WithJq(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--jq", ".data.items[].id"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -880,7 +895,7 @@ func TestApiCmd_MethodUppercase(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -899,7 +914,7 @@ func TestApiCmd_FileFlagParsing(t *testing.T) {
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
@@ -917,7 +932,7 @@ func TestApiCmd_FileAndOutputConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--file", "photo.jpg", "--output", "out.json"})
|
||||
@@ -934,7 +949,7 @@ func TestApiCmd_FileWithGET(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--file", "photo.jpg"})
|
||||
@@ -951,7 +966,7 @@ func TestApiCmd_FileStdinConflictWithData(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
})
|
||||
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--file", "-", "--data", "-"})
|
||||
@@ -974,7 +989,7 @@ func TestApiCmd_DryRunWithFile(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"POST", "/open-apis/im/v1/images", "--file", "image=" + tmpFile, "--data", `{"image_type":"message"}`, "--dry-run", "--as", "bot"})
|
||||
err := cmd.Execute()
|
||||
if err != nil {
|
||||
@@ -1015,7 +1030,7 @@ func TestApiCmd_PermissionError_DerivesFirstClassFields(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCmdApi(f, nil)
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"GET", "/open-apis/docx/v1/documents/test", "--as", "bot"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
@@ -1041,7 +1056,7 @@ func TestApiCmd_JsonFlag_Accepted(t *testing.T) {
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
258
shortcuts/doc/docs_history.go
Normal file
258
shortcuts/doc/docs_history.go
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type docsHistoryListSpec struct {
|
||||
Doc documentRef
|
||||
PageSize int
|
||||
PageToken string
|
||||
}
|
||||
|
||||
type docsHistoryRevertSpec struct {
|
||||
Doc documentRef
|
||||
HistoryVersionID string
|
||||
WaitTimeoutMs int
|
||||
}
|
||||
|
||||
type docsHistoryRevertStatusSpec struct {
|
||||
Doc documentRef
|
||||
TaskID string
|
||||
}
|
||||
|
||||
func parseDocsHistoryDocRef(raw, shortcut string) (documentRef, error) {
|
||||
ref, err := parseDocumentRef(raw)
|
||||
if err != nil {
|
||||
return documentRef{}, err
|
||||
}
|
||||
if ref.Kind == "doc" {
|
||||
return documentRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "docs %s only supports docx documents; use a docx token/URL or a wiki URL that resolves to docx", shortcut).WithParam("--doc")
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func validateDocsHistoryPageSize(pageSize int) error {
|
||||
if pageSize < 1 || pageSize > 20 {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --page-size %d: must be between 1 and 20", pageSize).WithParam("--page-size")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDocsHistoryVersionID(historyVersionID string) error {
|
||||
version, err := strconv.ParseInt(strings.TrimSpace(historyVersionID), 10, 64)
|
||||
if err != nil || version <= 0 {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--history-version-id must be a positive integer string returned by docs +history-list").WithParam("--history-version-id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDocsHistoryWaitTimeout(timeoutMs int) error {
|
||||
if timeoutMs < 0 || timeoutMs > 30000 {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --wait-timeout-ms %d: must be between 0 and 30000", timeoutMs).WithParam("--wait-timeout-ms")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func docsHistoryListParams(spec docsHistoryListSpec) map[string]interface{} {
|
||||
params := map[string]interface{}{
|
||||
"page_size": spec.PageSize,
|
||||
}
|
||||
if spec.PageToken != "" {
|
||||
params["page_token"] = spec.PageToken
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func docsHistoryRevertBody(spec docsHistoryRevertSpec) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"history_version_id": spec.HistoryVersionID,
|
||||
"wait_timeout_ms": spec.WaitTimeoutMs,
|
||||
}
|
||||
}
|
||||
|
||||
func docsHistoryStatusParams(spec docsHistoryRevertStatusSpec) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"task_id": spec.TaskID,
|
||||
}
|
||||
}
|
||||
|
||||
func docsHistoryAPIPath(docToken, suffix string) string {
|
||||
return fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/%s", validate.EncodePathSegment(docToken), suffix)
|
||||
}
|
||||
|
||||
var DocsHistoryList = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+history-list",
|
||||
Description: "List Lark document history versions",
|
||||
Risk: "read",
|
||||
Scopes: []string{"docx:document:readonly"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
PostMount: installDocsShortcutHelp("+history-list"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "document URL or token", Required: true},
|
||||
{Name: "page-size", Type: "int", Default: "20", Desc: "history entries to return, range 1-20"},
|
||||
{Name: "page-token", Desc: "pagination token from the previous page's page_token"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list"); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateDocsHistoryPageSize(runtime.Int("page-size"))
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list")
|
||||
spec := docsHistoryListSpec{
|
||||
Doc: ref,
|
||||
PageSize: runtime.Int("page-size"),
|
||||
PageToken: strings.TrimSpace(runtime.Str("page-token")),
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
Desc("OpenAPI: list document history versions").
|
||||
GET("/open-apis/docs_ai/v1/documents/:document_id/histories").
|
||||
Set("document_id", spec.Doc.Token).
|
||||
Params(docsHistoryListParams(spec))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list")
|
||||
spec := docsHistoryListSpec{
|
||||
Doc: ref,
|
||||
PageSize: runtime.Int("page-size"),
|
||||
PageToken: strings.TrimSpace(runtime.Str("page-token")),
|
||||
}
|
||||
|
||||
data, err := runtime.CallAPITyped(
|
||||
http.MethodGet,
|
||||
docsHistoryAPIPath(spec.Doc.Token, "histories"),
|
||||
docsHistoryListParams(spec),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.OutRaw(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var DocsHistoryRevert = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+history-revert",
|
||||
Description: "Revert a Lark document to a historical version",
|
||||
Risk: "write",
|
||||
Scopes: []string{"docx:document:write_only", "docx:document:readonly"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
PostMount: installDocsShortcutHelp("+history-revert"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "document URL or token", Required: true},
|
||||
{Name: "history-version-id", Desc: "history_version_id from docs +history-list to revert to", Required: true},
|
||||
{Name: "wait-timeout-ms", Type: "int", Default: "30000", Desc: "milliseconds to wait for revert completion before returning, range 0-30000"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDocsHistoryVersionID(runtime.Str("history-version-id")); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateDocsHistoryWaitTimeout(runtime.Int("wait-timeout-ms"))
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert")
|
||||
spec := docsHistoryRevertSpec{
|
||||
Doc: ref,
|
||||
HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")),
|
||||
WaitTimeoutMs: runtime.Int("wait-timeout-ms"),
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
Desc("OpenAPI: revert document history").
|
||||
POST("/open-apis/docs_ai/v1/documents/:document_id/history/revert").
|
||||
Set("document_id", spec.Doc.Token).
|
||||
Body(docsHistoryRevertBody(spec))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert")
|
||||
spec := docsHistoryRevertSpec{
|
||||
Doc: ref,
|
||||
HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")),
|
||||
WaitTimeoutMs: runtime.Int("wait-timeout-ms"),
|
||||
}
|
||||
|
||||
data, err := runtime.CallAPITyped(
|
||||
http.MethodPost,
|
||||
docsHistoryAPIPath(spec.Doc.Token, "history/revert"),
|
||||
nil,
|
||||
docsHistoryRevertBody(spec),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.OutRaw(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var DocsHistoryRevertStatus = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+history-revert-status",
|
||||
Description: "Get Lark document history revert task status",
|
||||
Risk: "read",
|
||||
Scopes: []string{"docx:document:readonly"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
PostMount: installDocsShortcutHelp("+history-revert-status"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "document URL or token", Required: true},
|
||||
{Name: "task-id", Desc: "task_id returned by docs +history-revert", Required: true},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status"); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("task-id")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--task-id is required").WithParam("--task-id")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status")
|
||||
spec := docsHistoryRevertStatusSpec{
|
||||
Doc: ref,
|
||||
TaskID: strings.TrimSpace(runtime.Str("task-id")),
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
Desc("OpenAPI: get document history revert status").
|
||||
GET("/open-apis/docs_ai/v1/documents/:document_id/history/revert_status").
|
||||
Set("document_id", spec.Doc.Token).
|
||||
Params(docsHistoryStatusParams(spec))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status")
|
||||
spec := docsHistoryRevertStatusSpec{
|
||||
Doc: ref,
|
||||
TaskID: strings.TrimSpace(runtime.Str("task-id")),
|
||||
}
|
||||
|
||||
data, err := runtime.CallAPITyped(
|
||||
http.MethodGet,
|
||||
docsHistoryAPIPath(spec.Doc.Token, "history/revert_status"),
|
||||
docsHistoryStatusParams(spec),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.OutRaw(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
309
shortcuts/doc/docs_history_test.go
Normal file
309
shortcuts/doc/docs_history_test.go
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestDocsHistoryValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shortcut common.Shortcut
|
||||
args []string
|
||||
param string
|
||||
}{
|
||||
{
|
||||
name: "list rejects legacy doc URL",
|
||||
shortcut: DocsHistoryList,
|
||||
args: []string{"+history-list", "--doc", "https://example.feishu.cn/doc/old_doc", "--as", "bot"},
|
||||
param: "--doc",
|
||||
},
|
||||
{
|
||||
name: "list rejects invalid page size",
|
||||
shortcut: DocsHistoryList,
|
||||
args: []string{"+history-list", "--doc", "doxcnHistory", "--page-size", "0", "--as", "bot"},
|
||||
param: "--page-size",
|
||||
},
|
||||
{
|
||||
name: "revert rejects invalid history version id",
|
||||
shortcut: DocsHistoryRevert,
|
||||
args: []string{"+history-revert", "--doc", "doxcnHistory", "--history-version-id", "0", "--as", "bot"},
|
||||
param: "--history-version-id",
|
||||
},
|
||||
{
|
||||
name: "revert rejects invalid wait timeout",
|
||||
shortcut: DocsHistoryRevert,
|
||||
args: []string{"+history-revert", "--doc", "doxcnHistory", "--history-version-id", "10", "--wait-timeout-ms", "30001", "--as", "bot"},
|
||||
param: "--wait-timeout-ms",
|
||||
},
|
||||
{
|
||||
name: "status rejects empty task id",
|
||||
shortcut: DocsHistoryRevertStatus,
|
||||
args: []string{"+history-revert-status", "--doc", "doxcnHistory", "--task-id", "", "--as", "bot"},
|
||||
param: "--task-id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-history-validation"))
|
||||
err := mountAndRunDocs(t, tt.shortcut, tt.args, f, stdout)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error, got nil")
|
||||
}
|
||||
_, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("error is not typed: %T %v", err, err)
|
||||
}
|
||||
var validationErr *errs.ValidationError
|
||||
if !errors.As(err, &validationErr) {
|
||||
t.Fatalf("expected validation error, got %T: %v", err, err)
|
||||
}
|
||||
if validationErr.Param != tt.param {
|
||||
t.Fatalf("param = %q, want %q (err: %v)", validationErr.Param, tt.param, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsHistoryDryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
listCmd := newDocsHistoryRuntimeCmd(t, DocsHistoryList, map[string]string{
|
||||
"doc": "doxcnHistoryDryRun",
|
||||
"page-size": "5",
|
||||
"page-token": "page_token_1",
|
||||
})
|
||||
listDry := decodeDocDryRun(t, DocsHistoryList.DryRun(context.Background(), common.TestNewRuntimeContext(listCmd, nil)))
|
||||
if got, want := listDry.API[0].URL, "/open-apis/docs_ai/v1/documents/doxcnHistoryDryRun/histories"; got != want {
|
||||
t.Fatalf("list dry-run URL = %q, want %q", got, want)
|
||||
}
|
||||
if got := int(listDry.API[0].Params["page_size"].(float64)); got != 5 {
|
||||
t.Fatalf("list page_size = %d, want 5", got)
|
||||
}
|
||||
if got := listDry.API[0].Params["page_token"]; got != "page_token_1" {
|
||||
t.Fatalf("list page_token = %#v, want page_token_1", got)
|
||||
}
|
||||
|
||||
revertCmd := newDocsHistoryRuntimeCmd(t, DocsHistoryRevert, map[string]string{
|
||||
"doc": "doxcnHistoryDryRun",
|
||||
"history-version-id": "42",
|
||||
"wait-timeout-ms": "30000",
|
||||
})
|
||||
revertDry := decodeDocDryRun(t, DocsHistoryRevert.DryRun(context.Background(), common.TestNewRuntimeContext(revertCmd, nil)))
|
||||
if got, want := revertDry.API[0].URL, "/open-apis/docs_ai/v1/documents/doxcnHistoryDryRun/history/revert"; got != want {
|
||||
t.Fatalf("revert dry-run URL = %q, want %q", got, want)
|
||||
}
|
||||
if got := revertDry.API[0].Body["history_version_id"]; got != "42" {
|
||||
t.Fatalf("revert history_version_id = %#v, want 42", got)
|
||||
}
|
||||
if got := int(revertDry.API[0].Body["wait_timeout_ms"].(float64)); got != 30000 {
|
||||
t.Fatalf("revert wait_timeout_ms = %d, want 30000", got)
|
||||
}
|
||||
|
||||
statusCmd := newDocsHistoryRuntimeCmd(t, DocsHistoryRevertStatus, map[string]string{
|
||||
"doc": "doxcnHistoryDryRun",
|
||||
"task-id": "task_1",
|
||||
})
|
||||
statusDry := decodeDocDryRun(t, DocsHistoryRevertStatus.DryRun(context.Background(), common.TestNewRuntimeContext(statusCmd, nil)))
|
||||
if got, want := statusDry.API[0].URL, "/open-apis/docs_ai/v1/documents/doxcnHistoryDryRun/history/revert_status"; got != want {
|
||||
t.Fatalf("status dry-run URL = %q, want %q", got, want)
|
||||
}
|
||||
if got := statusDry.API[0].Params["task_id"]; got != "task_1" {
|
||||
t.Fatalf("status task_id = %#v, want task_1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsHistoryExecuteList(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-history-list"))
|
||||
stub := &httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/docs_ai/v1/documents/doxcnHistory/histories",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"entries": []interface{}{
|
||||
map[string]interface{}{
|
||||
"revision_id": float64(42),
|
||||
"history_version_id": "11",
|
||||
"edit_time": "1780000000",
|
||||
"type": float64(1),
|
||||
"editor_ids": []interface{}{"ou_1"},
|
||||
},
|
||||
},
|
||||
"has_more": true,
|
||||
"page_token": "page_token_2",
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := mountAndRunDocs(t, DocsHistoryList, []string{
|
||||
"+history-list",
|
||||
"--doc", "doxcnHistory",
|
||||
"--page-size", "5",
|
||||
"--page-token", "page_token_1",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data := decodeDocsHistoryEnvelope(t, stdout)
|
||||
if got := data["page_token"]; got != "page_token_2" {
|
||||
t.Fatalf("page_token = %#v, want page_token_2", got)
|
||||
}
|
||||
entries, _ := data["entries"].([]interface{})
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("entries = %#v, want one entry", data["entries"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsHistoryExecuteRevert(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-history-revert"))
|
||||
stub := &httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/docs_ai/v1/documents/doxcnHistory/history/revert",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"task_id": "task_1",
|
||||
"status": "running",
|
||||
"history_version_id": "42",
|
||||
"poll_after_ms": float64(10000),
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := mountAndRunDocs(t, DocsHistoryRevert, []string{
|
||||
"+history-revert",
|
||||
"--doc", "doxcnHistory",
|
||||
"--history-version-id", "42",
|
||||
"--wait-timeout-ms", "0",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
|
||||
t.Fatalf("decode revert body: %v\nraw=%s", err, stub.CapturedBody)
|
||||
}
|
||||
if got := body["history_version_id"]; got != "42" {
|
||||
t.Fatalf("history_version_id = %#v, want 42", got)
|
||||
}
|
||||
if got := int(body["wait_timeout_ms"].(float64)); got != 0 {
|
||||
t.Fatalf("wait_timeout_ms = %d, want 0", got)
|
||||
}
|
||||
|
||||
data := decodeDocsHistoryEnvelope(t, stdout)
|
||||
if got := data["task_id"]; got != "task_1" {
|
||||
t.Fatalf("task_id = %#v, want task_1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsHistoryExecuteRevertStatus(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-history-status"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/docs_ai/v1/documents/doxcnHistory/history/revert_status",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"status": "partial_failed",
|
||||
"history_version_id": "11",
|
||||
"failed_block_tokens": []interface{}{"blk_1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRunDocs(t, DocsHistoryRevertStatus, []string{
|
||||
"+history-revert-status",
|
||||
"--doc", "doxcnHistory",
|
||||
"--task-id", "task_1",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data := decodeDocsHistoryEnvelope(t, stdout)
|
||||
if got := data["status"]; got != "partial_failed" {
|
||||
t.Fatalf("status = %#v, want partial_failed", got)
|
||||
}
|
||||
if got := data["history_version_id"]; got != "11" {
|
||||
t.Fatalf("history_version_id = %#v, want 11", got)
|
||||
}
|
||||
failed, _ := data["failed_block_tokens"].([]interface{})
|
||||
if len(failed) != 1 || failed[0] != "blk_1" {
|
||||
t.Fatalf("failed_block_tokens = %#v, want [blk_1]", data["failed_block_tokens"])
|
||||
}
|
||||
}
|
||||
|
||||
func newDocsHistoryRuntimeCmd(t *testing.T, shortcut common.Shortcut, values map[string]string) *cobra.Command {
|
||||
t.Helper()
|
||||
|
||||
cmd := &cobra.Command{Use: shortcut.Command}
|
||||
for _, flag := range shortcut.Flags {
|
||||
switch flag.Type {
|
||||
case "int":
|
||||
cmd.Flags().Int(flag.Name, 0, flag.Desc)
|
||||
default:
|
||||
cmd.Flags().String(flag.Name, flag.Default, flag.Desc)
|
||||
}
|
||||
}
|
||||
for name, value := range values {
|
||||
if err := cmd.Flags().Set(name, value); err != nil {
|
||||
t.Fatalf("set --%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func decodeDocsHistoryEnvelope(t *testing.T, stdout *bytes.Buffer) map[string]interface{} {
|
||||
t.Helper()
|
||||
|
||||
var envelope map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode envelope: %v\nraw=%s", err, stdout.String())
|
||||
}
|
||||
data, _ := envelope["data"].(map[string]interface{})
|
||||
if data == nil {
|
||||
t.Fatalf("missing data in envelope: %#v", envelope)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestDocsHistoryURLValidationMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := parseDocsHistoryDocRef("https://example.feishu.cn/doc/old_doc", "+history-list")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "only supports docx documents") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ func docsSkillReadCommandForShortcut(shortcut string) string {
|
||||
return docsSkillReadCommand + " references/lark-doc-fetch.md"
|
||||
case "update":
|
||||
return docsSkillReadCommand + " references/lark-doc-update.md"
|
||||
case "history-list", "history-revert", "history-revert-status":
|
||||
return docsSkillReadCommand + " references/lark-doc-history.md"
|
||||
default:
|
||||
return docsSkillReadCommand
|
||||
}
|
||||
@@ -44,6 +46,12 @@ func docsHelpCommandForShortcut(shortcut string) string {
|
||||
return "lark-cli docs +fetch --help"
|
||||
case "update":
|
||||
return "lark-cli docs +update --help"
|
||||
case "history-list":
|
||||
return "lark-cli docs +history-list --help"
|
||||
case "history-revert":
|
||||
return "lark-cli docs +history-revert --help"
|
||||
case "history-revert-status":
|
||||
return "lark-cli docs +history-revert-status --help"
|
||||
default:
|
||||
return "lark-cli docs --help"
|
||||
}
|
||||
@@ -56,6 +64,9 @@ func Shortcuts() []common.Shortcut {
|
||||
DocsCreate,
|
||||
DocsFetch,
|
||||
DocsUpdate,
|
||||
DocsHistoryList,
|
||||
DocsHistoryRevert,
|
||||
DocsHistoryRevertStatus,
|
||||
DocMediaInsert,
|
||||
DocMediaUpload,
|
||||
DocMediaPreview,
|
||||
|
||||
@@ -170,6 +170,27 @@ func TestRegisterShortcutsMountsDocsMediaPreview(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterShortcutsMountsDocsHistoryCommands(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newRegisterTestFactory(t))
|
||||
|
||||
for _, name := range []string{"+history-list", "+history-revert", "+history-revert-status"} {
|
||||
cmd, _, err := program.Find([]string{"docs", name})
|
||||
if err != nil {
|
||||
t.Fatalf("find docs %s shortcut: %v", name, err)
|
||||
}
|
||||
if cmd == nil || cmd.Name() != name {
|
||||
t.Fatalf("docs %s shortcut not mounted: %#v", name, cmd)
|
||||
}
|
||||
if cmd.Flags().Lookup("api-version") != nil {
|
||||
t.Fatalf("docs %s should not expose --api-version", name)
|
||||
}
|
||||
if !strings.Contains(cmd.Long, "lark-cli skills read lark-doc references/lark-doc-history.md") {
|
||||
t.Fatalf("docs %s help missing history skill guidance:\n%s", name, cmd.Long)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterShortcutsDocsHelpAddsSkillReadGuidance(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newRegisterTestFactory(t))
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "飞书云文档(Docx / Wiki 文档):读取和编辑飞书文
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["lark-cli"]
|
||||
cliHelp: "lark-cli docs --help; lark-cli docs +create --help; lark-cli docs +fetch --help; lark-cli docs +update --help; lark-cli docs +resource-download --help; lark-cli docs +resource-update --help; lark-cli docs +resource-delete --help"
|
||||
cliHelp: "lark-cli docs --help; lark-cli docs +create --help; lark-cli docs +fetch --help; lark-cli docs +update --help; lark-cli docs +history-list --help; lark-cli docs +history-revert --help; lark-cli docs +history-revert-status --help; lark-cli docs +resource-download --help; lark-cli docs +resource-update --help; lark-cli docs +resource-delete --help"
|
||||
---
|
||||
|
||||
# docs
|
||||
@@ -17,6 +17,7 @@ metadata:
|
||||
lark-cli docs +fetch --doc "文档URL或token"
|
||||
lark-cli docs +create --content '<title>标题</title><p>内容</p>'
|
||||
lark-cli docs +update --doc "文档URL或token" --command append --content '<p>内容</p>'
|
||||
lark-cli docs +history-list --doc "文档URL或token"
|
||||
```
|
||||
|
||||
## 前置条件 — 执行操作前必读
|
||||
@@ -25,6 +26,7 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '<p>
|
||||
1. [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) — 认证、权限处理、全局参数(所有操作通用)
|
||||
2. **读取文档(`docs +fetch`)** → 必读 [`lark-doc-fetch.md`](references/lark-doc-fetch.md)(`--scope` / `--detail` 选择、局部读取策略、`<fragment>` / `<excerpt>` 输出结构)
|
||||
3. **创建或编辑文档内容** → 必读 [`lark-doc-xml.md`](references/lark-doc-xml.md)(XML 语法规则,仅当用户明确要求 Markdown 时改读 [`lark-doc-md.md`](references/lark-doc-md.md))和 [`lark-doc-style.md`](references/style/lark-doc-style.md)(元素选择、丰富度规则、颜色语义);从零创建时加读 [`lark-doc-create-workflow.md`](references/style/lark-doc-create-workflow.md);编辑已有文档时加读 [`lark-doc-update.md`](references/lark-doc-update.md) 和 [`lark-doc-update-workflow.md`](references/style/lark-doc-update-workflow.md)
|
||||
4. **查看或回滚历史版本** → 必读 [`lark-doc-history.md`](references/lark-doc-history.md)(先 list 找 `history_version_id`,再 revert,必要时 status 轮询)
|
||||
|
||||
**未读完以上文件就执行相应操作会导致参数选择错误或格式错误。**
|
||||
|
||||
@@ -41,6 +43,7 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '<p>
|
||||
- 新增画板必须隔离到 SubAgent:简单图由 SubAgent 直接插入 `<whiteboard type="svg">完整 SVG</whiteboard>`,不读 `lark-whiteboard`;复杂图才由主 Agent 先建 `<whiteboard type="blank"></whiteboard>`,再启动 SubAgent 读取 `lark-whiteboard` 写入
|
||||
- 用户说"看一下文档里的图片/附件/素材""预览素材" → 用 `lark-cli docs +media-preview`
|
||||
- 用户明确说"下载素材" → 用 `lark-cli docs +media-download`
|
||||
- 用户想把文档回滚到某个 `revision_id` 或某一时刻 → 先读 [`lark-doc-history.md`](references/lark-doc-history.md),通过分页的 `docs +history-list` 匹配 `history_version_id`,再用 `docs +history-revert --history-version-id <id>` 回滚;不要把 `revision_id` 直接传给回滚命令。同一个 `revision_id` 可能对应多个 `history_version_id`:先拉一页,未命中则继续翻页;命中后最多额外再拉一页补齐相邻候选,然后按 `edit_time` 选择最接近用户目标时间的记录;没有目标时间或无法可靠区分时,再让用户确认。
|
||||
- 用户明确说"下载/更新/删除文档封面图" → 用 `lark-cli docs +resource-download/+resource-update/+resource-delete --type cover`
|
||||
- `resource-*` 目前仅支持 Docx 封面资源;其他图片、附件或素材请走 `+media-*`
|
||||
- 如果目标是画板/whiteboard/画板缩略图 → 只能用 `lark-cli docs +media-download --type whiteboard`(不要用 `+media-preview`)
|
||||
@@ -66,6 +69,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli docs +<verb> [flags]`)
|
||||
| [`+create`](references/lark-doc-create.md) | Create a Lark document (XML / Markdown) |
|
||||
| [`+fetch`](references/lark-doc-fetch.md) | Fetch Lark document content (XML / Markdown / im-markdown; `im-markdown` only after fetch for `lark-im`) |
|
||||
| [`+update`](references/lark-doc-update.md) | Update a Lark document (str_replace / block_insert_after / block_replace / ...) |
|
||||
| [`+history-list` / `+history-revert` / `+history-revert-status`](references/lark-doc-history.md) | List document history, revert to a `history_version_id`, and query revert task status |
|
||||
| [`+media-insert`](references/lark-doc-media-insert.md) | Insert a local image or file at the end of a Lark document (4-step orchestration + auto-rollback). Prefer `--from-clipboard` when the image is already on the system clipboard (screenshots, copy from Feishu/browser); use `--file` only for on-disk sources. |
|
||||
| [`+media-download`](references/lark-doc-media-download.md) | Download document media or whiteboard thumbnail (auto-detects extension) |
|
||||
| [`+media-preview`](references/lark-doc-media-preview.md) | Preview document media file (auto-detects extension) |
|
||||
|
||||
107
skills/lark-doc/references/lark-doc-history.md
Normal file
107
skills/lark-doc/references/lark-doc-history.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# docs history(历史版本与回滚)
|
||||
|
||||
用于查看 Docx 历史版本、按 `history_version_id` 回滚,以及查询回滚任务状态。
|
||||
|
||||
## 安全流程
|
||||
|
||||
1. 先用分页接口 `+history-list` 找到目标版本的 `history_version_id`。
|
||||
2. 如果用户指定的是 `revision_id`,不要假设它唯一,也不要把 `revision_id` 直接传给 `+history-revert`。先拉一页并在 `entries[]` 中筛选 `revision_id` 相同的候选;如果未匹配到且 `has_more=true`,继续用 `page_token` 翻页;如果已匹配到候选,最多额外再拉一页补齐可能跨页的相邻候选。最终优先根据用户目标时间与 `edit_time` 的接近程度选择最合适的一条,取同一条的 `history_version_id`;如果没有目标时间,或多个候选无法可靠区分,再向用户展示候选版本(`history_version_id`、`revision_id`、`edit_time`、`name/description`)并确认后回滚。
|
||||
3. 如果用户指定的是某一时刻但没有指定 `revision_id`,按 `entries[].edit_time` 匹配;优先选择不晚于目标时刻的最近一条历史记录,无法明确匹配时先向用户确认候选版本。
|
||||
4. 再用 `+history-revert --history-version-id <history_version_id>` 发起回滚。默认最多等待 30 秒;如果返回 `status: running`,记录 `task_id`。
|
||||
5. 用 `+history-revert-status` 轮询 `task_id`,直到状态不再是 `running`。
|
||||
6. 回滚完成后,用 `docs +fetch` 读取文档确认内容。
|
||||
|
||||
## 按 revision_id 或时间点回滚
|
||||
|
||||
当用户说“回滚到 revision_id=42”“恢复到昨天下午 3 点的版本”这类需求时,流程是:
|
||||
|
||||
1. 执行 `docs +history-list --doc <doc>` 获取第一页历史记录;`+history-list` 是分页接口,只有 `has_more=true` 且还需要更多候选时才继续传 `--page-token` 翻页。
|
||||
2. 如果用户给出 `revision_id`:先筛选当前页中 `entries[].revision_id == 用户给出的 revision_id`。如果未命中且 `has_more=true`,继续拉下一页;如果已经命中候选,最多额外再拉一页,补齐同一个 `revision_id` 可能跨页出现的相邻 `history_version_id`。若用户同时给出目标时间,在候选里选择 `edit_time` 与目标时间最接近的一条;若未给目标时间但候选只有一条,可直接使用;若多个候选无法可靠区分,不要自行取第一条,向用户展示候选并确认。
|
||||
3. 如果用户只给出时间:用 `entries[].edit_time` 匹配,选择目标时刻之前最近的一条;如果用户表达的是“最接近某时刻”,则选择绝对时间差最小的一条。
|
||||
4. 从最终匹配条目读取 `history_version_id`。`history_version_id` 对应服务端 `minor_history.version`,这是回滚接口需要的 ID。
|
||||
5. 执行 `docs +history-revert --doc <doc> --history-version-id <history_version_id>`。
|
||||
|
||||
候选确认时使用类似格式:
|
||||
|
||||
```text
|
||||
同一个 revision_id 命中多个历史版本,请确认要回滚哪一条:
|
||||
- history_version_id=11 revision_id=42 edit_time=2026-06-22T12:24:45Z name=...
|
||||
- history_version_id=12 revision_id=42 edit_time=2026-06-22T12:25:14Z name=...
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 列出历史版本
|
||||
lark-cli docs +history-list --doc "<docx_url_or_token>" --page-size 20
|
||||
|
||||
# 翻页
|
||||
lark-cli docs +history-list --doc "<docx_url_or_token>" --page-size 20 --page-token "<page_token>"
|
||||
|
||||
# 回滚到指定 history_version_id(默认等待 30000ms)
|
||||
lark-cli docs +history-revert --doc "<docx_url_or_token>" --history-version-id 42
|
||||
|
||||
# 只发起任务,不等待
|
||||
lark-cli docs +history-revert --doc "<docx_url_or_token>" --history-version-id 42 --wait-timeout-ms 0
|
||||
|
||||
# 查询回滚任务状态
|
||||
lark-cli docs +history-revert-status --doc "<docx_url_or_token>" --task-id "<task_id>"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 命令 | 参数 | 必填 | 说明 |
|
||||
|-|-|-|-|
|
||||
| `+history-list` | `--doc` | 是 | Docx URL/token,或可解析为 Docx 的 wiki URL |
|
||||
| `+history-list` | `--page-size` | 否 | 返回条数,范围 `1-20`,默认 `20` |
|
||||
| `+history-list` | `--page-token` | 否 | 上一页返回的 `page_token` |
|
||||
| `+history-revert` | `--doc` | 是 | Docx URL/token,或可解析为 Docx 的 wiki URL |
|
||||
| `+history-revert` | `--history-version-id` | 是 | `+history-list` 返回的 `history_version_id`,必须大于 0 |
|
||||
| `+history-revert` | `--wait-timeout-ms` | 否 | 等待回滚完成的毫秒数,范围 `0-30000`,默认 `30000` |
|
||||
| `+history-revert-status` | `--doc` | 是 | 同一个文档 |
|
||||
| `+history-revert-status` | `--task-id` | 是 | `+history-revert` 返回的 `task_id` |
|
||||
|
||||
## 返回值要点
|
||||
|
||||
`+history-list` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"revision_id": 42,
|
||||
"history_version_id": "11",
|
||||
"edit_time": "1780000000",
|
||||
"type": 1,
|
||||
"name": "版本名",
|
||||
"description": "版本说明",
|
||||
"editor_ids": ["ou_xxx"]
|
||||
}
|
||||
],
|
||||
"has_more": true,
|
||||
"page_token": "page_token"
|
||||
}
|
||||
```
|
||||
|
||||
`+history-revert` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "task_xxx",
|
||||
"status": "running",
|
||||
"history_version_id": "11",
|
||||
"poll_after_ms": 10000
|
||||
}
|
||||
```
|
||||
|
||||
`+history-revert-status` 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "partial_failed",
|
||||
"history_version_id": "11",
|
||||
"failed_block_tokens": ["blk_xxx"]
|
||||
}
|
||||
```
|
||||
|
||||
`status` 可能是 `running`、`done`、`partial_failed`、`failed`。当状态是 `partial_failed` 或 `failed` 时,优先检查 `failed_block_tokens`。
|
||||
@@ -1,9 +1,9 @@
|
||||
# Docs CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 8 leaf commands
|
||||
- Covered: 3
|
||||
- Coverage: 37.5%
|
||||
- Denominator: 11 leaf commands
|
||||
- Covered: 6
|
||||
- Coverage: 54.5%
|
||||
|
||||
## Summary
|
||||
- TestDocs_CreateAndFetchWorkflow: proves `docs +create` and `docs +fetch`; key `t.Run(...)` proof points are `create as bot` and `fetch as bot`.
|
||||
@@ -11,6 +11,8 @@
|
||||
- TestDocs_UpdateWorkflow: proves `docs +update` via `update-title-and-content as bot`, then re-fetches the same doc in `verify as bot` to assert persisted title/content changes.
|
||||
- TestDocs_DryRunDefaultsToV2OpenAPI: proves `docs +create`, `docs +fetch`, and `docs +update` dry-run all emit `/open-apis/docs_ai/v1/...` requests without MCP or `--api-version` guidance.
|
||||
- TestDocs_CreateTitleDryRunPrependsContent: proves `docs +create --title` dry-run prepends an escaped `<title>...</title>` tag to request body `content`.
|
||||
- TestDocs_DryRunDefaultsToV2OpenAPI also proves `docs +history-list`, `docs +history-revert`, and `docs +history-revert-status` dry-run endpoint and query/body shapes.
|
||||
- TestDocs_HistoryWorkflow proves the guarded live history flow (`LARK_DOC_HISTORY_E2E=1`): create, update, list prior revisions, revert, poll status when needed, and fetch to verify reverted content.
|
||||
- Setup note: docs workflows create a Drive folder through `drive files create_folder` in `helpers_test.go`; that helper is external to the docs domain and is not counted here.
|
||||
- Blocked area: media and search shortcuts still need deterministic fixtures and local file orchestration.
|
||||
|
||||
@@ -20,6 +22,9 @@
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/create; docs_update_dryrun_test.go::TestDocs_CreateTitleDryRunPrependsContent | `--parent-token`; `--doc-format markdown`; `--content`; `--title` | helper asserts returned doc id from `data.document.document_id`; dry-run asserts title is prepended into request body content |
|
||||
| ✓ | docs +fetch | shortcut | docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflow/fetch as bot; docs_update_test.go::TestDocs_UpdateWorkflow/verify as bot; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/fetch as user; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/fetch | `--doc <docToken>`; `--doc-format markdown` | |
|
||||
| ✓ | docs +history-list | shortcut | docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/history list; docs_history_workflow_test.go::TestDocs_HistoryWorkflow | `--doc`; `--page-size`; `--page-token` | live workflow gated by `LARK_DOC_HISTORY_E2E=1` |
|
||||
| ✓ | docs +history-revert | shortcut | docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/history revert; docs_history_workflow_test.go::TestDocs_HistoryWorkflow | `--doc`; `--history-version-id`; `--wait-timeout-ms` | live workflow gated by `LARK_DOC_HISTORY_E2E=1` |
|
||||
| ✓ | docs +history-revert-status | shortcut | docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/history revert status; docs_history_workflow_test.go::TestDocs_HistoryWorkflow | `--doc`; `--task-id` | live workflow polls only when revert returns `running` |
|
||||
| ✕ | docs +media-download | shortcut | | none | no media fixture workflow yet |
|
||||
| ✕ | docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions |
|
||||
| ✕ | docs +media-preview | shortcut | | none | requires deterministic media fixture |
|
||||
|
||||
135
tests/cli_e2e/docs/docs_history_workflow_test.go
Normal file
135
tests/cli_e2e/docs/docs_history_workflow_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/larksuite/cli/tests/cli_e2e/drive"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestDocs_HistoryWorkflow(t *testing.T) {
|
||||
if os.Getenv("LARK_DOC_HISTORY_E2E") != "1" {
|
||||
t.Skip("set LARK_DOC_HISTORY_E2E=1 to run docs history live workflow")
|
||||
}
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderName := "lark-cli-e2e-docs-history-folder-" + suffix
|
||||
docTitle := "lark-cli-e2e-docs-history-" + suffix
|
||||
originalMarker := "original history marker " + suffix
|
||||
updatedMarker := "updated history marker " + suffix
|
||||
const defaultAs = "user"
|
||||
|
||||
folderToken := drive.CreateDriveFolder(t, parentT, ctx, folderName, defaultAs, "")
|
||||
docToken := createDocWithRetry(t, parentT, ctx, folderToken, docTitle, originalMarker, defaultAs)
|
||||
|
||||
updateResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+update",
|
||||
"--doc", docToken,
|
||||
"--command", "overwrite",
|
||||
"--doc-format", "markdown",
|
||||
"--content", "# " + docTitle + "\n\n" + updatedMarker,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
updateResult.AssertExitCode(t, 0)
|
||||
updateResult.AssertStdoutStatus(t, true)
|
||||
|
||||
fetchUpdated, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"docs", "+fetch", "--doc", docToken, "--doc-format", "markdown"},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
fetchUpdated.AssertExitCode(t, 0)
|
||||
fetchUpdated.AssertStdoutStatus(t, true)
|
||||
updatedContent := gjson.Get(fetchUpdated.Stdout, "data.document.content").String()
|
||||
assert.Contains(t, updatedContent, updatedMarker)
|
||||
currentRevision := gjson.Get(fetchUpdated.Stdout, "data.document.revision_id").Int()
|
||||
require.Greater(t, currentRevision, int64(0), "stdout:\n%s", fetchUpdated.Stdout)
|
||||
|
||||
var revertHistoryVersionID string
|
||||
require.Eventually(t, func() bool {
|
||||
listResult, listErr := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+history-list",
|
||||
"--doc", docToken,
|
||||
"--page-size", "20",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
if listErr != nil || listResult.ExitCode != 0 {
|
||||
return false
|
||||
}
|
||||
for _, entry := range gjson.Get(listResult.Stdout, "data.entries").Array() {
|
||||
revisionID := entry.Get("revision_id").Int()
|
||||
historyVersionID := entry.Get("history_version_id").String()
|
||||
if revisionID > 0 && revisionID < currentRevision && historyVersionID != "" {
|
||||
revertHistoryVersionID = historyVersionID
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, 45*time.Second, 3*time.Second, "history list did not expose a prior revision")
|
||||
require.NotEmpty(t, revertHistoryVersionID)
|
||||
|
||||
revertResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+history-revert",
|
||||
"--doc", docToken,
|
||||
"--history-version-id", revertHistoryVersionID,
|
||||
"--wait-timeout-ms", "30000",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
revertResult.AssertExitCode(t, 0)
|
||||
revertResult.AssertStdoutStatus(t, true)
|
||||
|
||||
status := gjson.Get(revertResult.Stdout, "data.status").String()
|
||||
taskID := gjson.Get(revertResult.Stdout, "data.task_id").String()
|
||||
if status == "running" {
|
||||
require.NotEmpty(t, taskID, "stdout:\n%s", revertResult.Stdout)
|
||||
require.Eventually(t, func() bool {
|
||||
statusResult, statusErr := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"docs", "+history-revert-status",
|
||||
"--doc", docToken,
|
||||
"--task-id", taskID,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
if statusErr != nil || statusResult.ExitCode != 0 {
|
||||
return false
|
||||
}
|
||||
status = gjson.Get(statusResult.Stdout, "data.status").String()
|
||||
return status != "" && status != "running"
|
||||
}, 60*time.Second, 5*time.Second, "history revert task did not finish")
|
||||
}
|
||||
require.Equal(t, "done", status, "revert stdout:\n%s", revertResult.Stdout)
|
||||
|
||||
fetchReverted, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"docs", "+fetch", "--doc", docToken, "--doc-format", "markdown"},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
fetchReverted.AssertExitCode(t, 0)
|
||||
fetchReverted.AssertStdoutStatus(t, true)
|
||||
revertedContent := gjson.Get(fetchReverted.Stdout, "data.document.content").String()
|
||||
assert.Contains(t, revertedContent, originalMarker)
|
||||
assert.NotContains(t, revertedContent, updatedMarker)
|
||||
}
|
||||
@@ -24,9 +24,9 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantURL string
|
||||
name string
|
||||
args []string
|
||||
wantContains []string
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
@@ -35,7 +35,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
"--content", "<title>Dry Run</title><p>hello</p>",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/docs_ai/v1/documents",
|
||||
wantContains: []string{"/open-apis/docs_ai/v1/documents"},
|
||||
},
|
||||
{
|
||||
name: "create api-version v1 compatibility",
|
||||
@@ -45,7 +45,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
"--content", "<title>Dry Run</title><p>hello</p>",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/docs_ai/v1/documents",
|
||||
wantContains: []string{"/open-apis/docs_ai/v1/documents"},
|
||||
},
|
||||
{
|
||||
name: "fetch",
|
||||
@@ -54,7 +54,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
"--doc", "doxcnDryRunE2E",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/fetch",
|
||||
wantContains: []string{"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/fetch"},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
@@ -65,7 +65,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
"--content", "<p>hello</p>",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E",
|
||||
wantContains: []string{"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E"},
|
||||
},
|
||||
{
|
||||
name: "block_delete batch",
|
||||
@@ -76,7 +76,50 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
"--block-id", "blkA,blkB,blkC",
|
||||
"--dry-run",
|
||||
},
|
||||
wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E",
|
||||
wantContains: []string{"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E"},
|
||||
},
|
||||
{
|
||||
name: "history list",
|
||||
args: []string{
|
||||
"docs", "+history-list",
|
||||
"--doc", "doxcnDryRunE2E",
|
||||
"--page-size", "5",
|
||||
"--page-token", "page_token_1",
|
||||
"--dry-run",
|
||||
},
|
||||
wantContains: []string{
|
||||
"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/histories",
|
||||
`"page_size": 5`,
|
||||
`"page_token": "page_token_1"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "history revert",
|
||||
args: []string{
|
||||
"docs", "+history-revert",
|
||||
"--doc", "doxcnDryRunE2E",
|
||||
"--history-version-id", "42",
|
||||
"--wait-timeout-ms", "0",
|
||||
"--dry-run",
|
||||
},
|
||||
wantContains: []string{
|
||||
"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/history/revert",
|
||||
`"history_version_id": "42"`,
|
||||
`"wait_timeout_ms": 0`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "history revert status",
|
||||
args: []string{
|
||||
"docs", "+history-revert-status",
|
||||
"--doc", "doxcnDryRunE2E",
|
||||
"--task-id", "task_1",
|
||||
"--dry-run",
|
||||
},
|
||||
wantContains: []string{
|
||||
"/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/history/revert_status",
|
||||
`"task_id": "task_1"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -90,10 +133,7 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) {
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
combined := result.Stdout + "\n" + result.Stderr
|
||||
for _, want := range []string{
|
||||
tt.wantURL,
|
||||
"docs_ai/v1",
|
||||
} {
|
||||
for _, want := range append(tt.wantContains, "docs_ai/v1") {
|
||||
if !strings.Contains(combined, want) {
|
||||
t.Fatalf("dry-run output missing %q\nstdout:\n%s\nstderr:\n%s", want, result.Stdout, result.Stderr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user