Skip to content

Commit 1b8a750

Browse files
sebsnykclaude
andauthored
feat(docs): add page layout command
Add `gog docs page-layout <docId> [--layout=pageless|pages]` for toggling an existing Google Doc between pageless and pages mode through the Docs API. Maintainer fixups: - Made `page-layout` the canonical command to fit the existing noun-style docs command syntax, while preserving `set-page-layout` and `page-setup` aliases. - Kept command allow/deny filters compatible with the alias spellings. - Regenerated command docs and added changelog credit for @sebsnyk. Proof: - go test ./internal/cmd -run 'TestCommandFiltersHonorCanonicalAlias|TestDocsPageLayoutCmd|TestNormalizePageLayout|TestDocsCreate_Pageless' -count=1 - make ci - codex-review --parallel-tests "go test ./internal/cmd -run 'TestCommandFiltersHonorCanonicalAlias|TestDocsPageLayoutCmd|TestNormalizePageLayout|TestDocsCreate_Pageless' -count=1" - Live Google Docs test with [email protected]: created disposable doc 1WhA0EWTHbb2TzQQ0hQq4ErLb6Ju_GlLAgvnk-BoHyD8, wrote temporary content, ran `docs page-layout --layout=pages`, read raw Docs API JSON back as `documentStyle.documentFormat.documentMode=PAGES`, ran `docs page-layout --layout=pageless`, read back `PAGELESS`, then trashed the doc. - GitHub CI and docker checks green on 7b8c451. Co-authored-by: Sebastian Roth <[email protected]> Co-authored-by: Claude Opus 4.7 <[email protected]>
1 parent 0c5c5da commit 1b8a750

11 files changed

Lines changed: 356 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Calendar: add --with-zoom / --regenerate-zoom / --remove-zoom that create, regenerate, and remove Zoom meetings and attach the join URL + meeting ID + passcode to the Calendar event description. Google's Calendar API rejects conferenceData writes asserting `conferenceSolution.key.type="addOn"` from non-Workspace-Marketplace OAuth clients, so the description-mode integration is the path that round-trips through Google's storage; trade-off is no native "Join with Zoom" conference card. (#589, #590) — thanks @alexisperumal and @mvanhorn.
99
- Auth: add gog zoom auth setup / doctor for Zoom S2S OAuth credential storage. (#590) — thanks @mvanhorn.
1010
- Docs: add `gog docs insert-page-break <docId> [--index N | --at-end] [--tab=STRING]` to insert a Google Docs page break directly via `InsertPageBreakRequest` — markdown has no native page-break construct, so this is the only path for multi-page deliverables. Aliases: `page-break`, `pb`. (#604)
11+
- Docs: add `gog docs page-layout <docId> [--layout=pageless|pages]` to toggle the page layout of an existing Google Doc via `updateDocumentStyle` on `documentFormat.documentMode`. Sibling to the existing `--pageless` flag on `docs create`/`write`/`update` for the case where the doc was created upstream (e.g. by Drive markdown conversion) without the desired layout. Defaults to `pageless`. Aliases: `set-page-layout`, `page-setup`. (#593)
1112
- Docs: add `--heading-level N` (1..6 shortcut) and `--named-style NAME` (full enum) to `gog docs format` so existing paragraphs can be promoted to `HEADING_1`..`HEADING_6`, `TITLE`, `SUBTITLE`, or `NORMAL_TEXT`. Both set `paragraphStyle.namedStyleType` on the existing UpdateParagraphStyle request and compose cleanly with `--alignment` / `--line-spacing`. (#605)
1213
- Sheets: add `gog sheets reorder-tab <spreadsheetId> --tab=<name|sheetId> --to=N` to move a tab to a specific 0-based position via `updateSheetProperties` with field mask `index`. `--tab` accepts a title or a numeric sheet ID; `--to=0` is force-sent so the leftmost target reaches the wire as `"index":0`. Aliases: `move-tab`, `reorder-sheet`, `move-sheet`. (#603)
1314
- Docs: add `gog docs insert-table <docId> --rows N --cols M [--index N | --at-end] [--values-json [[...]]] [--tab=STRING]` to insert a native Google Docs table directly via `InsertTableRequest`, bypassing the markdown writer. `--values-json` takes a JSON 2D string array whose dimensions must match `--rows`x`--cols`. Empty `--values-json` produces an empty table structure. (#602)

docs/commands.generated.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Generated from `gog schema --json`.
241241
- [`gog docs (doc) insert-page-break (page-break,pb) <docId> [flags]`](commands/gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
242242
- [`gog docs (doc) insert-table --rows=INT --cols=INT <docId> [flags]`](commands/gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
243243
- [`gog docs (doc) list-tabs <docId>`](commands/gog-docs-list-tabs.md) - List all tabs in a Google Doc
244+
- [`gog docs (doc) page-layout (set-page-layout,page-setup) <docId> [flags]`](commands/gog-docs-page-layout.md) - Set page layout (pageless|pages) on an existing Google Doc
244245
- [`gog docs (doc) raw <docId> [flags]`](commands/gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
245246
- [`gog docs (doc) rename-tab <docId> [flags]`](commands/gog-docs-rename-tab.md) - Rename a tab in a Google Doc
246247
- [`gog docs (doc) sed <docId> [<expression>] [flags]`](commands/gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)

docs/commands/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.
44

5-
Generated pages: 567.
5+
Generated pages: 568.
66

77
## Top-level Commands
88

@@ -292,6 +292,7 @@ Generated pages: 567.
292292
- [gog docs insert-page-break](gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
293293
- [gog docs insert-table](gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
294294
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
295+
- [gog docs page-layout](gog-docs-page-layout.md) - Set page layout (pageless|pages) on an existing Google Doc
295296
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
296297
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
297298
- [gog docs sed](gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# `gog docs page-layout`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Set page layout (pageless|pages) on an existing Google Doc
6+
7+
## Usage
8+
9+
```bash
10+
gog docs (doc) page-layout (set-page-layout,page-setup) <docId> [flags]
11+
```
12+
13+
## Parent
14+
15+
- [gog docs](gog-docs.md)
16+
17+
## Flags
18+
19+
| Flag | Type | Default | Help |
20+
| --- | --- | --- | --- |
21+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
22+
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/drivelabels/docs/slides/contacts/tasks/people/sheets/forms/sites/appscript/analytics/searchconsole/ads/photos) |
23+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
24+
| `--color` | `string` | auto | Color output: auto\|always\|never |
25+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
26+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
27+
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
28+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
29+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
30+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
31+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
32+
| `--layout` | `string` | pageless | Page layout: pageless or pages |
33+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
34+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
35+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
36+
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
37+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
38+
| `--version` | `kong.VersionFlag` | | Print version and exit |
39+
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
40+
41+
## See Also
42+
43+
- [gog docs](gog-docs.md)
44+
- [Command index](README.md)

docs/commands/gog-docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ gog docs (doc) <command> [flags]
3333
- [gog docs insert-page-break](gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
3434
- [gog docs insert-table](gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
3535
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
36+
- [gog docs page-layout](gog-docs-page-layout.md) - Set page layout (pageless|pages) on an existing Google Doc
3637
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
3738
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
3839
- [gog docs sed](gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)

internal/cmd/docs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type DocsCmd struct {
4242
Clear DocsClearCmd `cmd:"" name:"clear" help:"Clear all content from a Google Doc"`
4343
Structure DocsStructureCmd `cmd:"" name:"structure" aliases:"struct" help:"Show document structure with numbered paragraphs"`
4444
Raw DocsRawCmd `cmd:"" name:"raw" help:"Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)"`
45+
PageLayout DocsPageLayoutCmd `cmd:"" name:"page-layout" aliases:"set-page-layout,page-setup" help:"Set page layout (pageless|pages) on an existing Google Doc"`
4546
}
4647

4748
type DocsTabsCmd struct {

internal/cmd/docs_helpers.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,23 @@ func docsWebViewLink(id string) string {
5555
return "https://docs.google.com/document/d/" + id + "/edit"
5656
}
5757

58+
const (
59+
docsDocumentModePages = "PAGES"
60+
docsDocumentModePageless = "PAGELESS"
61+
)
62+
5863
func setDocumentPageless(ctx context.Context, svc *docs.Service, docID string) error {
64+
return setDocumentMode(ctx, svc, docID, docsDocumentModePageless)
65+
}
66+
67+
// setDocumentMode toggles documentStyle.documentFormat.documentMode via a
68+
// single batchUpdate call.
69+
func setDocumentMode(ctx context.Context, svc *docs.Service, docID, mode string) error {
5970
_, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{
6071
Requests: []*docs.Request{{
6172
UpdateDocumentStyle: &docs.UpdateDocumentStyleRequest{
6273
DocumentStyle: &docs.DocumentStyle{
63-
DocumentFormat: &docs.DocumentFormat{DocumentMode: "PAGELESS"},
74+
DocumentFormat: &docs.DocumentFormat{DocumentMode: mode},
6475
},
6576
Fields: "documentFormat",
6677
},
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/steipete/gogcli/internal/outfmt"
10+
"github.com/steipete/gogcli/internal/ui"
11+
)
12+
13+
// DocsPageLayoutCmd toggles the page layout on an existing Google Doc.
14+
// The Docs UI exposes this via File → Page setup → Pageless/Pages. The Docs
15+
// API exposes it via documents.batchUpdate with updateDocumentStyle on the
16+
// documentFormat.documentMode field. See setDocumentMode in docs_helpers.go.
17+
//
18+
// Sibling to the --pageless flag on `docs create` / `docs write` for the case
19+
// where the doc already exists (e.g. created by Drive markdown conversion in
20+
// an upstream step that didn't set the layout).
21+
type DocsPageLayoutCmd struct {
22+
DocID string `arg:"" name:"docId" help:"Doc ID"`
23+
Layout string `name:"layout" enum:"pageless,pages,paged" default:"pageless" help:"Page layout: pageless or pages"`
24+
}
25+
26+
func (c *DocsPageLayoutCmd) Run(ctx context.Context, flags *RootFlags) error {
27+
u := ui.FromContext(ctx)
28+
docID := strings.TrimSpace(c.DocID)
29+
if docID == "" {
30+
return usage("empty docId")
31+
}
32+
33+
mode, err := normalizePageLayout(c.Layout)
34+
if err != nil {
35+
return err
36+
}
37+
38+
if dryRunErr := dryRunExit(ctx, flags, "docs.page-layout", map[string]any{
39+
"documentId": docID,
40+
"layout": c.Layout,
41+
"mode": mode,
42+
}); dryRunErr != nil {
43+
return dryRunErr
44+
}
45+
46+
svc, err := requireDocsService(ctx, flags)
47+
if err != nil {
48+
return err
49+
}
50+
51+
if err := setDocumentMode(ctx, svc, docID, mode); err != nil {
52+
if isDocsNotFound(err) {
53+
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
54+
}
55+
return fmt.Errorf("set page layout: %w", err)
56+
}
57+
58+
if outfmt.IsJSON(ctx) {
59+
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
60+
"documentId": docID,
61+
"layout": c.Layout,
62+
"mode": mode,
63+
})
64+
}
65+
66+
u.Out().Linef("documentId\t%s", docID)
67+
u.Out().Linef("layout\t%s", c.Layout)
68+
return nil
69+
}
70+
71+
func normalizePageLayout(layout string) (string, error) {
72+
switch strings.ToLower(strings.TrimSpace(layout)) {
73+
case "pageless":
74+
return docsDocumentModePageless, nil
75+
case "paged", "pages":
76+
return docsDocumentModePages, nil
77+
case "":
78+
return "", usage("empty --layout (expected pageless or pages)")
79+
default:
80+
return "", usage(fmt.Sprintf("invalid --layout %q (expected pageless or pages)", layout))
81+
}
82+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
11+
"google.golang.org/api/docs/v1"
12+
)
13+
14+
func TestDocsPageLayoutCmd_PagelessDefault(t *testing.T) {
15+
origDocs := newDocsService
16+
t.Cleanup(func() { newDocsService = origDocs })
17+
18+
var batchRequests [][]*docs.Request
19+
var targetDocID string
20+
21+
docSvc, cleanup := newDocsServiceForTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22+
switch {
23+
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, ":batchUpdate"):
24+
// Capture the doc ID from the path: /v1/documents/{id}:batchUpdate
25+
path := strings.TrimPrefix(r.URL.Path, "/v1/documents/")
26+
targetDocID = strings.TrimSuffix(path, ":batchUpdate")
27+
var req docs.BatchUpdateDocumentRequest
28+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
29+
t.Fatalf("decode: %v", err)
30+
}
31+
batchRequests = append(batchRequests, req.Requests)
32+
w.Header().Set("Content-Type", "application/json")
33+
_ = json.NewEncoder(w).Encode(map[string]any{"documentId": "doc1"})
34+
default:
35+
http.NotFound(w, r)
36+
}
37+
}))
38+
defer cleanup()
39+
newDocsService = func(context.Context, string) (*docs.Service, error) { return docSvc, nil }
40+
41+
flags := &RootFlags{Account: "[email protected]"}
42+
ctx := newDocsCmdContext(t)
43+
44+
if err := runKong(t, &DocsPageLayoutCmd{}, []string{"doc1"}, ctx, flags); err != nil {
45+
t.Fatalf("page-layout: %v", err)
46+
}
47+
48+
if targetDocID != "doc1" {
49+
t.Fatalf("expected batchUpdate on doc1, got %q", targetDocID)
50+
}
51+
if len(batchRequests) != 1 || len(batchRequests[0]) != 1 {
52+
t.Fatalf("expected 1 batch request with 1 op, got %#v", batchRequests)
53+
}
54+
upd := batchRequests[0][0].UpdateDocumentStyle
55+
if upd == nil {
56+
t.Fatalf("expected UpdateDocumentStyle, got %#v", batchRequests[0][0])
57+
}
58+
if upd.Fields != "documentFormat" {
59+
t.Fatalf("expected fields=documentFormat, got %q", upd.Fields)
60+
}
61+
if upd.DocumentStyle == nil || upd.DocumentStyle.DocumentFormat == nil {
62+
t.Fatalf("expected DocumentStyle.DocumentFormat, got %#v", upd.DocumentStyle)
63+
}
64+
if upd.DocumentStyle.DocumentFormat.DocumentMode != docsDocumentModePageless {
65+
t.Fatalf("expected documentMode=PAGELESS, got %q", upd.DocumentStyle.DocumentFormat.DocumentMode)
66+
}
67+
}
68+
69+
func TestDocsPageLayoutCmd_Pages(t *testing.T) {
70+
origDocs := newDocsService
71+
t.Cleanup(func() { newDocsService = origDocs })
72+
73+
var batchRequests [][]*docs.Request
74+
75+
docSvc, cleanup := newDocsServiceForTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
76+
switch {
77+
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, ":batchUpdate"):
78+
var req docs.BatchUpdateDocumentRequest
79+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
80+
t.Fatalf("decode: %v", err)
81+
}
82+
batchRequests = append(batchRequests, req.Requests)
83+
w.Header().Set("Content-Type", "application/json")
84+
_ = json.NewEncoder(w).Encode(map[string]any{"documentId": "doc1"})
85+
default:
86+
http.NotFound(w, r)
87+
}
88+
}))
89+
defer cleanup()
90+
newDocsService = func(context.Context, string) (*docs.Service, error) { return docSvc, nil }
91+
92+
flags := &RootFlags{Account: "[email protected]"}
93+
ctx := newDocsCmdContext(t)
94+
95+
if err := runKong(t, &DocsPageLayoutCmd{}, []string{"doc1", "--layout=pages"}, ctx, flags); err != nil {
96+
t.Fatalf("page-layout pages: %v", err)
97+
}
98+
99+
if len(batchRequests) != 1 {
100+
t.Fatalf("expected 1 batch request, got %d", len(batchRequests))
101+
}
102+
upd := batchRequests[0][0].UpdateDocumentStyle
103+
if upd == nil || upd.DocumentStyle == nil || upd.DocumentStyle.DocumentFormat == nil {
104+
t.Fatalf("unexpected request shape: %#v", batchRequests[0][0])
105+
}
106+
if upd.DocumentStyle.DocumentFormat.DocumentMode != docsDocumentModePages {
107+
t.Fatalf("expected documentMode=PAGES, got %q", upd.DocumentStyle.DocumentFormat.DocumentMode)
108+
}
109+
}
110+
111+
func TestDocsPageLayoutCmd_EmptyDocID(t *testing.T) {
112+
flags := &RootFlags{Account: "[email protected]"}
113+
ctx := newDocsCmdContext(t)
114+
err := runKong(t, &DocsPageLayoutCmd{}, []string{""}, ctx, flags)
115+
if err == nil || !strings.Contains(err.Error(), "empty docId") {
116+
t.Fatalf("expected empty docId error, got %v", err)
117+
}
118+
}
119+
120+
func TestDocsPageLayoutCmd_InvalidLayoutRejected(t *testing.T) {
121+
flags := &RootFlags{Account: "[email protected]"}
122+
ctx := newDocsCmdContext(t)
123+
err := runKong(t, &DocsPageLayoutCmd{}, []string{"doc1", "--layout=portrait"}, ctx, flags)
124+
if err == nil {
125+
t.Fatalf("expected enum validation error, got nil")
126+
}
127+
}
128+
129+
func TestNormalizePageLayout(t *testing.T) {
130+
cases := []struct {
131+
in string
132+
want string
133+
wantErr bool
134+
}{
135+
{"pageless", docsDocumentModePageless, false},
136+
{"PAGELESS", docsDocumentModePageless, false},
137+
{"paged", docsDocumentModePages, false},
138+
{"pages", docsDocumentModePages, false},
139+
{" Paged ", docsDocumentModePages, false},
140+
{"", "", true},
141+
{"weird", "", true},
142+
}
143+
for _, tc := range cases {
144+
got, err := normalizePageLayout(tc.in)
145+
if tc.wantErr {
146+
if err == nil {
147+
t.Errorf("normalizePageLayout(%q): expected error, got %q", tc.in, got)
148+
}
149+
continue
150+
}
151+
if err != nil {
152+
t.Errorf("normalizePageLayout(%q): unexpected error: %v", tc.in, err)
153+
continue
154+
}
155+
if got != tc.want {
156+
t.Errorf("normalizePageLayout(%q): got %q, want %q", tc.in, got, tc.want)
157+
}
158+
}
159+
}
160+
161+
func TestDocsPageLayoutCmd_DryRun(t *testing.T) {
162+
origDocs := newDocsService
163+
t.Cleanup(func() { newDocsService = origDocs })
164+
165+
newDocsService = func(context.Context, string) (*docs.Service, error) {
166+
t.Fatal("docs service should not be created on dry-run")
167+
return nil, errors.New("unexpected docs service creation")
168+
}
169+
170+
flags := &RootFlags{Account: "[email protected]", DryRun: true}
171+
ctx := newDocsJSONContext(t)
172+
173+
err := (&DocsPageLayoutCmd{DocID: "doc1", Layout: "pageless"}).Run(ctx, flags)
174+
var exitErr *ExitError
175+
if err == nil {
176+
t.Fatalf("expected dry-run ExitError, got nil")
177+
}
178+
if !errors.As(err, &exitErr) || exitErr.Code != 0 {
179+
t.Fatalf("expected dry-run exit 0, got %v", err)
180+
}
181+
}

0 commit comments

Comments
 (0)