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.
127 lines
5.3 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|