Files
larksuite__cli/shortcuts/sheets/flag_schema_test.go
zhengzhijiej-tech 5f35c72bd3 feat(sheets): combine chart workflows and special chart types (#2374)
* feat(sheets): support partial chart snapshot schemas

* feat(sheets): add semantic chart shortcuts

* feat(sheets): improve semantic chart workflows

* fix(sheets): prefer semantic chart shortcuts

* fix(sheets): normalize chart range and flag inputs

* fix(sheets): normalize irregular chart ranges

* feat(sheets): add chart data update shortcut

* feat(sheets): harden chart update workflows

* feat(sheets): improve chart creation dimension handling

* feat(sheets): add dedicated chart batch shortcuts

* fix(sheets): allow chart color theme patches

* fix(sheets): persist chart color theme updates

* feat(sheets): simplify batch chart operations

* fix(sheets): preserve batch scope and cross-sheet chart ranges

* feat(sheets): support bubble waterfall and pareto charts

* fix(sheets): sync special chart tool schema

* feat(sheets): add semantic bubble chart indexes

* docs(sheets): sync combined chart workflow guidance

* fix(sheets): align combined chart artifacts

* feat(sheets): add x-axis number interpretation flag

* docs(sheets): validate chart axis semantics

* fix(sheets): allow recursive chart update patches

* test(sheets): isolate chart create schema check

* feat(sheets): refine semantic chart creation

* docs(sheets): sync semantic chart guidance

* docs(sheets): remove unrelated label position guidance

* fix(sheets): support all chart data label combinations

* fix(sheets): support numeric x-axis bounds

* feat(sheets): support chart y-axis bounds

* feat(sheets): add last-point chart label flag

* fix(sheets): preserve disabled waterfall stacking

* fix(sheets): nest last-point chart label property

* fix(sheets): resolve lint and dead-code CI failures

- lark_sheet_chart.go: drop redundant chartConfigUpdateInput /
  chartDataUpdateInput calls in Execute whose result is immediately
  overwritten by the *FromSnapshot variant (ineffassign); the snapshot
  variants already re-run the same validation internally.
- lark_sheet_chart_test.go: remove Go 1.22+ redundant loop-variable
  copies (copyloopvar).
- batch_op_dispatch.go / lark_sheet_batch_update.go: remove unreachable
  allowedBatchShortcuts and batchUpdateInput; callers use the lower-level
  allowedShortcuts and buildBatchUpdatePlan directly.

* fix(sheets): validate chart config updates

* fix(sheets): sync skill specs and chart schema validation

* fix(sheets): resolve chart review follow-ups

* fix(sheets): restore the two-color contract

* fix(sheets): require at least two chart colors

* docs(sheets): expose advanced chart shortcut flags

* fix(sheets): address chart review feedback

* fix(sheets): surface ignored batch locators

* chore(sheets): bump skill version to 3.1.6

* fix(sheets): surface batch warnings consistently

* test(sheets): satisfy copyloopvar lint

* docs(sheets): sync skill from spec

* fix(sheets): harden chart batch updates

* fix(sheets): tighten chart update validation

* fix(sheets): canonicalize chart ranges and batch targets
2026-08-25 18:53:47 +08:00

349 lines
12 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
)
// TestFlagSchemas_EmbedParses asserts the synced flag-schemas.json
// embedded blob is valid JSON and has at least one shortcut/flag entry.
// If sync_to_consumers.mjs ever ships an empty or broken artifact, this
// catches it at build time of the test binary.
func TestFlagSchemas_EmbedParses(t *testing.T) {
t.Parallel()
idx, err := loadFlagSchemas()
if err != nil {
t.Fatalf("loadFlagSchemas error: %v", err)
}
if idx == nil || len(idx.Flags) == 0 {
t.Fatalf("flag-schemas.json has no entries")
}
if idx.SchemaVersion == "" {
t.Errorf("schema_version missing")
}
// Spot-check a couple of canonical entries we know upstream guarantees.
for _, want := range []string{"+cells-set", "+chart-create", "+batch-update"} {
if _, ok := idx.Flags[want]; !ok {
t.Errorf("missing shortcut entry %q (regenerate via sheet-skill-spec/scripts/sync_to_consumers.mjs)", want)
}
}
}
// TestPrintFlagSchema_ListIntrospectable verifies that calling the
// closure with an empty flag name returns the JSON listing of
// introspectable flags for the shortcut.
func TestPrintFlagSchema_ListIntrospectable(t *testing.T) {
t.Parallel()
out, err := printFlagSchemaFor("+cells-set")("")
if err != nil {
t.Fatalf("err: %v", err)
}
var got map[string]interface{}
if err := json.Unmarshal(out, &got); err != nil {
t.Fatalf("output not JSON: %v\n%s", err, out)
}
if got["shortcut"] != "+cells-set" {
t.Errorf("shortcut = %v, want +cells-set", got["shortcut"])
}
flags, _ := got["introspectable_flags"].([]interface{})
if len(flags) == 0 || flags[0] != "cells" {
t.Errorf("introspectable_flags = %v, want [cells]", flags)
}
}
// TestPrintFlagSchema_NamedFlagReturnsSchemaSubtree verifies a hit on
// (+chart-create, properties) yields a JSON Schema object with the
// expected top-level fields.
func TestPrintFlagSchema_NamedFlagReturnsSchemaSubtree(t *testing.T) {
t.Parallel()
out, err := printFlagSchemaFor("+chart-create")("properties")
if err != nil {
t.Fatalf("err: %v", err)
}
var schema map[string]interface{}
if err := json.Unmarshal(out, &schema); err != nil {
t.Fatalf("output not JSON: %v\n%s", err, out)
}
if schema["type"] != "object" {
t.Errorf("schema.type = %v, want object", schema["type"])
}
if _, ok := schema["properties"]; !ok {
t.Errorf("schema missing nested .properties: keys=%v", keysOf(schema))
}
}
// TestPrintFlagSchema_ChartUpdateIsRecursivePartial keeps introspection aligned
// with update validation: callers may patch a deeply nested field without
// resending required siblings from the full chart snapshot.
func TestPrintFlagSchema_ChartUpdateIsRecursivePartial(t *testing.T) {
t.Parallel()
decode := func(t *testing.T, command, path string) map[string]interface{} {
t.Helper()
out, err := printFlagSchemaFor(command)(path)
if err != nil {
t.Fatalf("print %s %s: %v", command, path, err)
}
var schema map[string]interface{}
if err := json.Unmarshal(out, &schema); err != nil {
t.Fatalf("schema is not JSON: %v\n%s", err, out)
}
return schema
}
createPlot := decode(t, "+chart-create", "properties.snapshot.plotArea.plot")
if required, _ := createPlot["required"].([]interface{}); len(required) == 0 {
t.Fatal("chart-create plot schema must retain required fields")
}
updatePlot := decode(t, "+chart-update", "properties.snapshot.plotArea.plot")
if _, present := updatePlot["required"]; present {
t.Fatalf("chart-update plot schema must be partial; required=%v", updatePlot["required"])
}
updateSeriesItem := decode(t, "+chart-update", "properties.snapshot.data.dim2.series.items")
required, _ := updateSeriesItem["required"].([]interface{})
if len(required) != 1 || required[0] != "index" {
t.Fatalf("chart-update replacement array item schema must retain required index; required=%v", required)
}
}
// TestPrintFlagSchema_UnknownFlagListsAvailable confirms the error
// message tells the caller which flags exist for the shortcut.
func TestPrintFlagSchema_UnknownFlagListsAvailable(t *testing.T) {
t.Parallel()
_, err := printFlagSchemaFor("+chart-create")("does-not-exist")
ve := requireValidation(t, err, "+chart-create")
if !strings.Contains(ve.Message, "properties") {
t.Errorf("message should list available flags; got %q", ve.Message)
}
if ve.Param != "--flag-name" {
t.Errorf("param = %q, want --flag-name", ve.Param)
}
}
// TestPrintFlagSchema_UnknownShortcut surfaces a missing shortcut entry.
func TestPrintFlagSchema_UnknownShortcut(t *testing.T) {
t.Parallel()
_, err := printFlagSchemaFor("+not-a-real-shortcut")("")
if err == nil {
t.Fatal("expected error for unknown shortcut")
}
}
// TestShortcuts_AttachesPrintFlagSchema confirms the registration loop
// in Shortcuts() wires PrintFlagSchema onto each shortcut whose command
// has a schema entry, and leaves it nil for shortcuts that don't.
func TestShortcuts_AttachesPrintFlagSchema(t *testing.T) {
t.Parallel()
all := Shortcuts()
withSchema := commandsWithFlagSchema()
for _, s := range all {
_, expected := withSchema[s.Command]
got := s.PrintFlagSchema != nil
if got != expected {
t.Errorf("%s: PrintFlagSchema attached=%v, expected=%v", s.Command, got, expected)
}
}
}
// TestPrintSchema_SystemFlagShortCircuit verifies the framework's
// --print-schema interception: required flags are relaxed, Validate /
// Execute are skipped, and the schema JSON appears on stdout.
func TestPrintSchema_SystemFlagShortCircuit(t *testing.T) {
t.Parallel()
// +cells-set has required --range / --cells / --sheet-id; without
// --print-schema, cobra would reject the call. With --print-schema,
// it should print the schema and exit cleanly. The PrintFlagSchema
// closure is normally attached by Shortcuts(), so we attach it here
// to mirror that registration path.
sc := CellsSet
sc.PrintFlagSchema = printFlagSchemaFor(sc.Command)
stdout, err := runShortcut(t, sc, []string{"--print-schema", "--flag-name", "cells"})
if err != nil {
t.Fatalf("err: %v\nstdout=%s", err, stdout)
}
if !strings.Contains(stdout, "\"type\"") {
t.Errorf("expected JSON Schema with \"type\" key; got=%s", stdout)
}
}
// TestPrintSchema_ListingWhenNoFlagNameGiven exercises the discovery
// path: `--print-schema` without `--flag-name` should list the
// shortcut's introspectable flags as JSON on stdout.
func TestPrintSchema_ListingWhenNoFlagNameGiven(t *testing.T) {
t.Parallel()
sc := CellsSet
sc.PrintFlagSchema = printFlagSchemaFor(sc.Command)
stdout, err := runShortcut(t, sc, []string{"--print-schema"})
if err != nil {
t.Fatalf("err: %v\nstdout=%s", err, stdout)
}
var got map[string]interface{}
if err := json.Unmarshal([]byte(stdout), &got); err != nil {
t.Fatalf("stdout not JSON: %v\n%s", err, stdout)
}
flags, _ := got["introspectable_flags"].([]interface{})
if len(flags) == 0 {
t.Errorf("introspectable_flags empty: %#v", got)
}
}
// TestPrintSchema_SystemFlagAbsentForReadOnlyShortcut ensures we don't
// inject --print-schema onto shortcuts that have no composite flags.
// +workbook-info is read-only and not in the schema map.
func TestPrintSchema_SystemFlagAbsentForReadOnlyShortcut(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, WorkbookInfo, []string{"--url", testURL, "--print-schema"})
if err == nil {
t.Fatal("expected unknown flag error")
}
if !strings.Contains(err.Error(), "unknown flag") {
t.Errorf("expected 'unknown flag'; got %v", err)
}
}
// TestPrintSchema_UnknownFlagNameIsStructured pins issue #6: an unregistered
// --flag-name passed to --print-schema must surface as a typed
// *errs.ValidationError, not a bare error string, so the agent-facing
// introspection path stays machine-parseable.
func TestPrintSchema_UnknownFlagNameIsStructured(t *testing.T) {
t.Parallel()
// PrintFlagSchema is wired during registration (shortcuts.go), not on the
// literal, so replicate that here to make Mount inject the --print-schema /
// --flag-name system flags.
sc := CellsSet
sc.PrintFlagSchema = printFlagSchemaFor(sc.Command)
_, _, err := runShortcutCapturingErr(t, sc, []string{
"--print-schema", "--flag-name", "nonexistent",
})
if err == nil {
t.Fatal("expected an error for --print-schema with an unregistered flag name")
}
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("error type = %T, want a typed *errs.ValidationError", err)
}
}
func keysOf(m map[string]interface{}) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// TestPrintSchema_DottedPathSlicing covers --flag-name's dotted-path form,
// which had no tests at all: disabling the implicit items/oneOf descent, or the
// explicit "items" segment, broke nothing.
//
// The feature exists so agents can pull one subtree out of chart-create's
// ~1,750-line properties schema instead of paging the whole dump (SKILL.md
// points at it by name). A silent regression pushes them straight back to full
// dumps, which is invisible in any output-correctness test.
func TestPrintSchema_DottedPathSlicing(t *testing.T) {
t.Parallel()
print := printFlagSchemaFor("+chart-create")
decode := func(t *testing.T, raw []byte) map[string]interface{} {
t.Helper()
var node map[string]interface{}
if err := json.Unmarshal(raw, &node); err != nil {
t.Fatalf("schema slice is not a JSON object: %v", err)
}
return node
}
props := func(t *testing.T, node map[string]interface{}) map[string]interface{} {
t.Helper()
p, ok := node["properties"].(map[string]interface{})
if !ok {
t.Fatalf("node has no properties: %v", node)
}
return p
}
t.Run("one segment walks into properties", func(t *testing.T) {
t.Parallel()
raw, err := print("properties.snapshot")
if err != nil {
t.Fatalf("slice failed: %v", err)
}
if _, has := props(t, decode(t, raw))["plotArea"]; !has {
t.Errorf("snapshot subtree should expose plotArea, got %s", raw)
}
})
t.Run("array levels are descended implicitly", func(t *testing.T) {
t.Parallel()
// data.refs is an array; naming the field must land on the ITEM shape,
// not force the caller to spell ".items".
raw, err := print("properties.snapshot.data.refs")
if err != nil {
t.Fatalf("slice failed: %v", err)
}
node := decode(t, raw)
if node["type"] != "array" {
t.Errorf("refs should still be the array node, got %v", node["type"])
}
deeper, err := print("properties.snapshot.data.refs.value")
if err != nil {
t.Fatalf("descending through array items failed: %v", err)
}
if len(deeper) == 0 {
t.Error("expected the item's value field")
}
})
t.Run("an explicit items segment also works", func(t *testing.T) {
t.Parallel()
if _, err := print("properties.snapshot.plotArea.axes.items"); err != nil {
t.Fatalf("explicit items segment failed: %v", err)
}
})
t.Run("a slice is strictly smaller than the whole flag schema", func(t *testing.T) {
t.Parallel()
full, err := print("properties")
if err != nil {
t.Fatalf("full dump failed: %v", err)
}
slice, err := print("properties.snapshot.plotArea.axes")
if err != nil {
t.Fatalf("slice failed: %v", err)
}
if len(slice) >= len(full) {
t.Errorf("slice is %d bytes vs %d for the full schema — slicing saves nothing", len(slice), len(full))
}
})
t.Run("a miss names the keys actually available", func(t *testing.T) {
t.Parallel()
_, err := print("properties.snapshot.nope")
if err == nil {
t.Fatal("want an error for an unknown segment")
}
ve := requireValidation(t, err, `no "nope" under properties.snapshot`)
if !strings.Contains(ve.Message, "plotArea") {
t.Errorf("the miss must list the reachable keys so the caller can retry without a full dump, got %q", ve.Message)
}
if ve.Param != "--flag-name" {
t.Errorf("param = %q, want --flag-name", ve.Param)
}
})
t.Run("the underscore spelling of the flag still resolves", func(t *testing.T) {
t.Parallel()
if _, err := printFlagSchemaFor("+cells-set-style")("border_styles"); err != nil {
t.Fatalf("underscore flag name should resolve to border-styles: %v", err)
}
})
}