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.
50 lines
1.9 KiB
Go
50 lines
1.9 KiB
Go
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageSheetAndMinutesSmallRemainingBranches(t *testing.T) {
|
|
if _, err := buildStyleCells(&styleSpec{FontColorsJSON: "{"}, 1, 1); err == nil {
|
|
t.Fatal("invalid font colors JSON returned nil")
|
|
}
|
|
|
|
root := &cobra.Command{Use: "sheet"}
|
|
group := &cobra.Command{Use: "range", Run: func(*cobra.Command, []string) {}}
|
|
group.AddCommand(&cobra.Command{Use: "read", Run: func(*cobra.Command, []string) {}})
|
|
root.AddCommand(group)
|
|
attachUnknownSubcommandGuard(root)
|
|
if err := root.RunE(root, []string{"read"}); err == nil || !strings.Contains(err.Error(), "range read") {
|
|
t.Fatalf("deep suggestion err=%v", err)
|
|
}
|
|
|
|
installScriptedCaller(t, &scriptedToolCaller{dry: true})
|
|
if err := executeFilterCoverage(t, newMinutesCommand(), "permission", "add", "--ids", "id", "--member-uids", "uid", "--policy", "3", "--cover=false"); err != nil {
|
|
t.Fatalf("explicit false cover: %v", err)
|
|
}
|
|
if err := executeFilterCoverage(t, newMinutesCommand(), "list", "all", "--start", "2030-01-01T10:00:00+08:00", "--end", "2030-01-01T09:00:00+08:00"); err == nil {
|
|
t.Fatal("reversed minutes range returned nil")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageGeminiForwardRemainingFailures(t *testing.T) {
|
|
invalid := &geminiAPIForwarder{baseURL: "%", model: "model", timeout: time.Second}
|
|
if _, err := invalid.forward(context.Background(), "", "text"); err == nil {
|
|
t.Fatal("invalid Gemini endpoint returned nil")
|
|
}
|
|
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("transport")
|
|
})}
|
|
f := &geminiAPIForwarder{baseURL: "https://example.test", model: "model", apiKey: "key", timeout: time.Second, httpClient: client}
|
|
if _, err := f.forward(context.Background(), "", "text"); err == nil || !strings.Contains(err.Error(), "transport") {
|
|
t.Fatalf("Gemini transport err=%v", err)
|
|
}
|
|
}
|