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
41 changes: 41 additions & 0 deletions tests/cli_e2e/base/base_core_workflow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"
"testing"
"time"

clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

func TestBase_CoreWorkflow(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
t.Cleanup(cancel)

baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base"))
Comment on lines +17 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Isolate the CLI config for this E2E test.

This shells out to the real CLI without sandboxing LARKSUITE_CLI_CONFIG_DIR, so it can read/write shared local state and cross-contaminate other runs.

Suggested fix
 func TestBase_CoreWorkflow(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
 	t.Cleanup(cancel)
As per coding guidelines, `**/*_test.go`: Isolate config state in Go tests by using `t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestBase_CoreWorkflow(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
t.Cleanup(cancel)
baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base"))
func TestBase_CoreWorkflow(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
t.Cleanup(cancel)
baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/base/base_core_workflow_test.go` around lines 17 - 21, The test
TestBase_CoreWorkflow currently shells out to the real CLI and can read/write
shared config; isolate its CLI config by setting the env var
LARKSUITE_CLI_CONFIG_DIR to a temp dir for the test (use
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())) before invoking
createBase/uniqueName so the CLI uses the test-specific config directory and
avoids cross-test contamination.


t.Run("get base", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+base-get", "--base-token", baseToken},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot base get capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.base.name").String(), "stdout:\n%s", result.Stdout)
})

t.Run("copy base", func(t *testing.T) {
copiedToken := copyBase(t, ctx, baseToken, uniqueName("lark-cli-e2e-base-copy"))
assert.NotEqual(t, baseToken, copiedToken)
})
}
265 changes: 265 additions & 0 deletions tests/cli_e2e/base/base_dashboard_form_workflow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"
"testing"
"time"

clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

func TestBase_DashboardWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
t.Cleanup(cancel)
Comment on lines +17 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Isolate the CLI config in both E2E entrypoints.

Both tests shell out to the real CLI without sandboxing LARKSUITE_CLI_CONFIG_DIR, so they can inherit and mutate shared config/auth state.

Suggested fix
 func TestBase_DashboardWorkflow(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	parentT := t
 	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
 	t.Cleanup(cancel)
@@
 func TestBase_FormWorkflow(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	parentT := t
 	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
 	t.Cleanup(cancel)
As per coding guidelines, `**/*_test.go`: Isolate config state in Go tests by using `t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())`.

Also applies to: 142-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/base/base_dashboard_form_workflow_test.go` around lines 17 -
20, The tests call the real CLI and must isolate shared CLI config; update the
entrypoints (e.g., TestBase_DashboardWorkflow and the other test referenced
around lines 142-145) to set a sandboxed config dir by calling
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start (before any CLI
shell-out) so each test uses its own temporary config directory and cannot
inherit or mutate global auth/state.


baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-dashboard"))
tableID, _, _ := createTable(t, parentT, ctx, baseToken, uniqueName("DashboardTable"), `[{"name":"Amount","type":"number"}]`, "")
dashboardID := createDashboard(t, parentT, ctx, baseToken, uniqueName("Sales Dashboard"))
blockID := createBlock(t, parentT, ctx, baseToken, dashboardID, "Amount Stats", "statistics", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"sum"}],"count_all":true}`)
Comment on lines +22 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the actual generated table name in dashboard block config.

The table is created with uniqueName("DashboardTable"), but both block payloads still reference the literal "DashboardTable". That makes this workflow target the wrong table or fail when no plain DashboardTable exists.

Suggested fix
-	tableID, _, _ := createTable(t, parentT, ctx, baseToken, uniqueName("DashboardTable"), `[{"name":"Amount","type":"number"}]`, "")
-	blockID := createBlock(t, parentT, ctx, baseToken, dashboardID, "Amount Stats", "statistics", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"sum"}],"count_all":true}`)
+	tableName := uniqueName("DashboardTable")
+	_, _, _ := createTable(t, parentT, ctx, baseToken, tableName, `[{"name":"Amount","type":"number"}]`, "")
+	blockID := createBlock(t, parentT, ctx, baseToken, dashboardID, "Amount Stats", "statistics", `{"table_name":"`+tableName+`","series":[{"field_name":"Amount","rollup":"sum"}],"count_all":true}`)
@@
-			Args:      []string{"base", "+dashboard-block-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--name", "Amount Stats Updated", "--data-config", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"SUM"}],"count_all":true}`},
+			Args:      []string{"base", "+dashboard-block-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--name", "Amount Stats Updated", "--data-config", `{"table_name":"`+tableName+`","series":[{"field_name":"Amount","rollup":"SUM"}],"count_all":true}`},
@@
-	_ = tableID

Also applies to: 97-99, 139-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cli_e2e/base/base_dashboard_form_workflow_test.go` around lines 22 -
25, The dashboard block payloads are referencing the literal "DashboardTable"
instead of the actual generated name created by uniqueName; update the calls
that build block JSON (used by createBlock) to interpolate or pass the generated
table name returned/used by createTable (e.g., store the uniqueName result in a
variable like tableName or use the value returned by createTable) and replace
all occurrences of the string "DashboardTable" in the block config (including
the series.field_name/table_name entries) with that variable so the block
targets the real table; adjust the calls near createTable and createBlock (and
the other occurrences at lines referenced in the comment) accordingly.


t.Run("dashboard list", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-list", "--base-token", baseToken},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard list capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.True(t, gjson.Get(result.Stdout, "data.items.#(dashboard_id==\""+dashboardID+"\")").Exists(), "stdout:\n%s", result.Stdout)
})

t.Run("dashboard get", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-get", "--base-token", baseToken, "--dashboard-id", dashboardID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard get capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, dashboardID, gjson.Get(result.Stdout, "data.dashboard.dashboard_id").String())
})

t.Run("dashboard update", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--name", "Sales Dashboard Updated", "--theme-style", "SimpleBlue"},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard update capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Sales Dashboard Updated", gjson.Get(result.Stdout, "data.dashboard.name").String())
})

t.Run("dashboard block list", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-block-list", "--base-token", baseToken, "--dashboard-id", dashboardID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard block list capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.True(t, gjson.Get(result.Stdout, "data.items.#(block_id==\""+blockID+"\")").Exists(), "stdout:\n%s", result.Stdout)
})

t.Run("dashboard block get", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-block-get", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard block get capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, blockID, gjson.Get(result.Stdout, "data.block.block_id").String())
})

t.Run("dashboard block update", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-block-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--name", "Amount Stats Updated", "--data-config", `{"table_name":"DashboardTable","series":[{"field_name":"Amount","rollup":"SUM"}],"count_all":true}`},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard block update capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Amount Stats Updated", gjson.Get(result.Stdout, "data.block.name").String())
})

t.Run("dashboard block delete", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-block-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--block-id", blockID, "--yes"},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard block delete capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, blockID, gjson.Get(result.Stdout, "data.block_id").String())
})

t.Run("dashboard delete", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+dashboard-delete", "--base-token", baseToken, "--dashboard-id", dashboardID, "--yes"},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot dashboard delete capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, dashboardID, gjson.Get(result.Stdout, "data.dashboard_id").String())
})

_ = tableID
}

func TestBase_FormWorkflow(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
t.Cleanup(cancel)

baseToken := createBase(t, ctx, uniqueName("lark-cli-e2e-base-form"))
tableID, _, _ := createTable(t, parentT, ctx, baseToken, uniqueName("FormTable"), `[{"name":"Name","type":"text"}]`, "")
formID := createForm(t, parentT, ctx, baseToken, tableID, uniqueName("Survey"))

var questionID string

t.Run("form get", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-get", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form get capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, formID, gjson.Get(result.Stdout, "data.id").String())
})

t.Run("form list", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-list", "--base-token", baseToken, "--table-id", tableID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form list capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.True(t, gjson.Get(result.Stdout, "data.forms.#(id==\""+formID+"\")").Exists(), "stdout:\n%s", result.Stdout)
})

t.Run("form update", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-update", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--name", "Survey Updated", "--description", "updated description"},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form update capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Survey Updated", gjson.Get(result.Stdout, "data.name").String())
})

t.Run("form questions create", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-questions-create", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--questions", `[{"type":"text","title":"Your Name","required":true}]`},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form question create capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
questionID = gjson.Get(result.Stdout, "data.questions.0.id").String()
require.NotEmpty(t, questionID, "stdout:\n%s", result.Stdout)
})

t.Run("form questions list", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-questions-list", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form question list capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.True(t, gjson.Get(result.Stdout, "data.questions.#(id==\""+questionID+"\")").Exists(), "stdout:\n%s", result.Stdout)
})

t.Run("form questions update", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-questions-update", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--questions", `[{"id":"` + questionID + `","title":"Your Name Updated","required":true}]`},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form question update capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Your Name Updated", gjson.Get(result.Stdout, "data.questions.0.title").String())
})

t.Run("form questions delete", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-questions-delete", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--question-ids", `["` + questionID + `"]`},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form question delete capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, true, gjson.Get(result.Stdout, "data.deleted").Bool(), "stdout:\n%s", result.Stdout)
})

t.Run("form delete", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"base", "+form-delete", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--yes"},
DefaultAs: "bot",
})
require.NoError(t, err)
if result.ExitCode != 0 {
skipIfBaseUnavailable(t, result, "requires bot form delete capability")
}
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, true, gjson.Get(result.Stdout, "data.deleted").Bool(), "stdout:\n%s", result.Stdout)
})
}
Loading
Loading