Write testcase code, not framework code. tests/cli_e2e/core.go already provides the harness, and tests/cli_e2e/demo/task_lifecycle_test.go is the reference example only. Unless the user explicitly asks for framework work, add or update testcase files only.
A good cli e2e testcase here is:
This is different from traditional API test suites where usage docs live elsewhere. Here, the command contract is discoverable from lark-cli --help, domain help, subcommand help, and schema output, and the agent is expected to explore and verify it autonomously.
Put real domain testcases under:
tests/cli_e2e/{domain}/
Examples:
tests/cli_e2e/task/task_status_workflow_test.go
tests/cli_e2e/task/task_comment_workflow_test.go
Treat tests/cli_e2e/demo/ as reference material, not as the place to accumulate real coverage.
Split by feature or workflow, not by API surface inventory.
Good splits:
create -> complete -> get -> reopen -> get
Bad split:
task_test.go that creates a task, updates it, comments it, reminds it, assigns it, adds followers, attaches tasklists, and queries everything in one lifecyclePrefer:
Do not guess command names, flags, or payload fields from memory. Discover them:
lark-cli --help
lark-cli <domain> --help
lark-cli <domain> +<shortcut> -h
lark-cli <domain> <resource> <method> -h
lark-cli schema <domain>.<resource>.<method>
Use this exploration loop repeatedly while writing the testcase:
--params and --data shapeAlso inspect environmental constraints before finalizing coverage:
bot, user, or bothCall clie2e.RunCmd with clie2e.Request.
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"task", "tasks", "get"},
Params: map[string]any{
"task_guid": taskGUID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, 0)
Use Request like this:
Args: command path and plain flagsParams: JSON for --params
Data: JSON for --data
BinaryPath, DefaultAs, Format: only when the testcase must override defaultsUse one top-level test per workflow. Break the workflow into substeps with t.Run.
func TestDomain_Scenario(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
suffix := time.Now().UTC().Format("20060102-150405")
var resourceID string
t.Run("create", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{...})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
resourceID = gjson.Get(result.Stdout, "data.guid").String()
require.NotEmpty(t, resourceID)
parentT.Cleanup(func() {
// best-effort delete
})
})
t.Run("get", func(t *testing.T) {
require.NotEmpty(t, resourceID)
})
}
Use this shape because:
t.Run makes reports readableparentT.Cleanup keeps created resources alive for later substepsPrefer workflows whose data can be created and cleaned up entirely within the testcase.
Good:
Be explicit when the data is not self-consistent:
t.Skip() and a short reasonExample:
func TestTask_AssignWorkflow_UserOnly(t *testing.T) {
t.Skip("requires a real user open_id and user-capable test environment")
}
Do not silently hardcode made-up IDs, fake URLs, or guessed remote resources just to make the testcase look complete.
Assume the current local/CI-like environment may support only bot identity by default.
Implications:
--as user worksWhen --as user is unavailable:
t.Skip()
Typical risky areas:
+get-my-tasks
t.Run for lifecycle steps such as create, update, get, list, delete.t.Cleanup for teardown and shared cleanup.t.Helper() in local helpers when the same setup or assertion logic really repeats.require.NoError for command execution and prerequisites.assert for returned field values after the command has succeeded.gjson.Get(result.Stdout, "...") for JSON field extraction.{"ok": true, ...} and should use result.AssertStdoutStatus(t, true)
{"code": 0, "data": ...} and should use result.AssertStdoutStatus(t, 0)
Then assert the business fields with gjson.
tests/cli_e2e/core.go just because one testcase wants a convenience wrapper.lark-cli-e2e- or <domain>-e2e-.tests/cli_e2e/demo/.Params or Data fields when schema output can tell you the exact shape.t.Skip() with a concrete reason.go test ./tests/cli_e2e/... -count=1.