Files
dingtalk-workspace-cli/internal/helpers/sheet_style_coverage_more_test.go
T
huwenjiao.hwj 0bb2b6ce98 feat(sheet): CSV export, style extensions and create-with-data
Adds four capabilities and, for the style surface, moves to the interface that
can actually express them.

Added:
- sheet create --values / --sheets / --styles: create a workbook and populate it
  in one command. --values takes a 2D array into the default sheet, --sheets
  takes typed tables across several sheets, --styles carries cell_styles /
  row_sizes / col_sizes / cell_merges. Every structure and enum is validated
  before the document is created, so an invalid config never leaves an orphan
  empty document behind.
- sheet export --export-format csv: synchronous single-sheet RFC4180 export with
  --sheet-id, --range and --value-render-option. --output writes to a file (a
  directory gets sheet-export.csv), otherwise the CSV goes to stdout while the
  truncation warning goes to stderr, keeping stdout pipeable.
- sheet update-dimension --size-type: pixel / standard (restore the default row
  height or column width) / auto (fit row height to content, ROWS only).
- sheet replace --match-formula: search and replace inside formula text.
- sheet range set-style --font-style / --font-line / --font-family /
  --border-styles-json.
- sheet range batch-set-style --ranges: stamp one style across several
  sheet-qualified ranges.

Changed (breaking Schema change, no CLI break):
- sheet range set-style moves from update_range to set_cell_range. The
  update_range style channel exposes exactly eight properties
  (backgroundColors, fontSizes, horizontalAlignments, verticalAlignments,
  fontColors, fontWeights, wordWrap, numberFormat) and has no slot for italic,
  underline/line-through, font family or borders, so the four new dimensions are
  not expressible there. interface_ref becomes set_cell_range and the twelve
  style flags stop publishing a flat property, because the value now lands in
  cells[i][j].cellStyles.* with no single top-level field to name.
- sheet range batch-set-style submits one atomic batch_update instead of looping
  update_range, so a partial failure no longer leaves half the ranges stamped.
  --continue-on-error becomes a server passthrough. Caps the fan-out at 100
  ranges and 200000 cells in aggregate.
- sheet export and sheet create declare interface_mode=composite: both route
  across several tools depending on the flags, so a single mcp ref was wrong.

Schema hygiene:
- Nineteen parameters that previously resolved through flag_name_inference into
  property names present in no request (bgColor, exportFormat, values, ...) are
  now reviewed mapping exclusions with a stated reason, so Schema omits the
  property instead of inventing one.

schema-compatibility reports 17 changed fields: 13 on sheet.range_set_style
(interface_ref plus twelve property mappings) and 2 each on
sheet.submit_export_job and sheet.create_workspace_sheet (interface_mode plus
interface_ref). No CLI flag is removed and no command path changes; the same
invocation runs on both the old and the new binary. Landing this needs a
decision on the Schema contract break.

Tests: targeted sheet suites in internal/helpers and the interface disposition
contract tests in internal/app pass; make build and gofmt clean;
check-schema-catalog, check-generated-drift, check-command-surface,
check-skill-commands and check-runtime-confirmation-truth all pass.
2026-08-07 20:25:21 +08:00

127 lines
5.3 KiB
Go

package helpers
import (
"errors"
"io"
"os"
"path/filepath"
"testing"
)
func TestCrossPlatformCoverageSheetStyleRangeAndFontCoverage(t *testing.T) {
for _, raw := range []string{"", "Sheet1!A1:B2", "B2:A1", "A1", "A", "1", "A0", "A-1", "A1:bad"} {
_, _, _ = parseA1Range(raw)
}
for _, tc := range []struct {
scalar int
jsonStr string
}{
{1, `[[1]]`}, {-1, ""}, {12, ""}, {0, `{`}, {0, `[[1,2]]`}, {0, `[[1]]`}, {0, ""},
} {
if get, err := intGrid(tc.scalar, tc.jsonStr, "font-size", 1, 1); err == nil && get != nil {
_, _ = get(0, 0)
_, _ = get(9, 9)
}
}
for _, tc := range []struct {
scalar, raw string
enum map[string]bool
}{
{"x", `[["x"]]`, nil}, {"bad", "", hAlignEnum}, {"left", "", hAlignEnum},
{"", "", nil}, {"", `{`, nil}, {"", `[["x","y"]]`, nil},
{"", `[[""]]`, hAlignEnum}, {"", `[["bad"]]`, hAlignEnum}, {"", `[["left"]]`, hAlignEnum},
} {
if get, err := strGrid(tc.scalar, tc.raw, "align", tc.enum, 1, 1); err == nil && get != nil {
_, _ = get(0, 0)
_, _ = get(9, 9)
}
}
for _, raw := range []string{"", `{`, `{}`, `{"nope":{"style":"solid"}}`, `{"top":{}}`,
`{"top":{"style":"bogus"}}`, `{"top":{"style":"solid"}}`, `{"top":{"style":"solid","color":"#000"}}`} {
_, _ = parseBorderStyles(raw)
}
for _, raw := range []string{"Sheet1!A1:B2", "A1:B2", "Sheet1!", "!A1"} {
_, _, _ = splitSheetPrefixedRange(raw, 0)
}
_ = maxColLenStr([][]string{{}, {"a", "b"}})
_ = maxColLen2D([][]int{{}, {1, 2}})
_ = checkMatrixShape(1, 1, 1, 1, "matrix")
_ = checkMatrixShape(1, 2, 1, 1, "matrix")
for _, tc := range []struct {
spec styleSpec
rows, cols int
}{
{styleSpec{}, 0, 1}, {styleSpec{}, 1001, 1}, {styleSpec{}, 1000, 31},
{styleSpec{WordWrap: "invalid"}, 1, 1}, {styleSpec{WordWrap: "clip"}, 1, 1},
{styleSpec{NumberFormat: "General"}, 1, 1}, {styleSpec{}, 1, 1},
{styleSpec{FontStyle: "bogus"}, 1, 1}, {styleSpec{FontStyle: "italic"}, 1, 1},
{styleSpec{FontLine: "bogus"}, 1, 1}, {styleSpec{FontLine: "underline"}, 1, 1},
{styleSpec{FontLine: "line-through"}, 1, 1}, {styleSpec{FontLine: "none"}, 1, 1},
{styleSpec{FontFamily: "Arial"}, 1, 1},
{styleSpec{BorderStylesJSON: `{"top":{"style":"solid"}}`}, 1, 1},
{styleSpec{BorderStylesJSON: `{`}, 1, 1},
{styleSpec{BgColor: "#FFF", FontSize: 12, HAlign: "center", VAlign: "top",
FontColor: "#000", FontWeight: "bold"}, 2, 2},
} {
_, _ = buildStyleCells(&tc.spec, tc.rows, tc.cols)
}
}
func TestCrossPlatformCoverageSheetBatchSetStyleCoverage(t *testing.T) {
oldArgs := os.Args
os.Args = []string{"dws", "sheet"}
t.Cleanup(func() { os.Args = oldArgs })
write := func(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "batch.json")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
return path
}
cases := []struct {
name string
body string
path string
format string
response string
callErr error
cont bool
}{
{name: "missing-file", path: filepath.Join(t.TempDir(), "missing")},
{name: "invalid-json", body: `{`},
{name: "empty", body: `[]`},
{name: "missing-fields-stop", body: `[{"sheetId":""}]`},
{name: "missing-fields-continue", body: `[{"sheetId":""},{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, cont: true},
{name: "invalid-range", body: `[{"sheetId":"Sheet1","range":"bad","bgColor":"red"}]`},
{name: "invalid-range-continue", body: `[{"sheetId":"Sheet1","range":"bad","bgColor":"red"},{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, cont: true},
{name: "invalid-style", body: `[{"sheetId":"Sheet1","range":"A1"}]`},
{name: "invalid-style-continue", body: `[{"sheetId":"Sheet1","range":"A1"},{"sheetId":"Sheet1","range":"A2","bgColor":"red"}]`, cont: true},
{name: "json-success", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "json", response: `{"ok":true}`},
{name: "json-raw-result", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "json", response: `{`},
{name: "json-call-error-stop", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "json", callErr: errors.New("failed")},
{name: "json-call-error-continue", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"},{"sheetId":"Sheet1","range":"A2","fontSize":12}]`, format: "json", callErr: errors.New("failed"), cont: true},
{name: "raw-success", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "raw", response: `{"ok":true}`},
{name: "raw-call-error", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "raw", callErr: errors.New("failed"), cont: true},
{name: "raw-call-error-stop", body: `[{"sheetId":"Sheet1","range":"A1","bgColor":"red"}]`, format: "raw", callErr: errors.New("failed")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
caller := &helpersCoreCaller{format: tc.format, err: tc.callErr, result: textToolResult(tc.response)}
installHelpersCoreDeps(t, caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
cmd := newRangeBatchSetStyleCmd()
_ = cmd.Flags().Set("node", "node")
path := tc.path
if path == "" {
path = write(t, tc.body)
}
_ = cmd.Flags().Set("batch", path)
_ = cmd.Flags().Set("continue-on-error", map[bool]string{true: "true", false: "false"}[tc.cont])
_ = cmd.RunE(cmd, nil)
})
}
}