From de4ed913a2df5ff18b25beadc91d7d25f38463a1 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Wed, 19 Aug 2026 16:04:37 +0800 Subject: [PATCH] docs(status-page): correct the command names in the README and e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status page group is registered as `status-page`, but the README and the e2e suite still used `statuspage` with subcommands (`changes`, `create-incident`, `create-timeline`, space-separated `migrate ...`) and a `--id` flag that never existed. Anyone following them — a person or an agent reading the docs — got "unknown command". Corrected against the registrations in zz_generated_status_pages.go, and documented the two things that trip up a first call: `change-create` takes page-id as a positional that overrides the `--data` key, and the required `updates` array has no flag, so every real call carries `--data` (or `--data -` to read the body from stdin). The e2e suite is behind the e2e build tag and skips without an app key, so it was not failing CI, but it was broken in both directions: the positive tests could not have passed, and the negative ones passed for the wrong reason — asserting a validation error while actually getting "unknown command". Two further defects surfaced while fixing them: the list helpers unmarshalled a top-level array where the CLI prints an {"items":[...]} envelope, and the header assertion named columns the reflective heuristic never emits. Two tests had no valid replacement verb and were retargeted rather than deleted: `list` has no --id filter, so single-page lookup became `status-page info`, and the invalid-id case moved to `change-active-list`. The generated fences and the flashduty skill card were already correct and are untouched. --- README.md | 103 ++++++++++++++++++++++++---- README_zh.md | 99 +++++++++++++++++++++++++-- e2e/auth_global_test.go | 2 +- e2e/edge_case_test.go | 2 +- e2e/resource_list_test.go | 20 ++++-- e2e/statuspage_test.go | 140 +++++++++++++++++++++++++------------- 6 files changed, 290 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 17922e8..3c87627 100644 --- a/README.md +++ b/README.md @@ -186,32 +186,105 @@ flashduty field list [flags] # List custom field definitions Supports `--name`. -### `statuspage` - Status Page Management (5 command groups) +### `status-page` - Status Page Management (28 commands) + +The group is `status-page` (hyphenated), not `statuspage`. Nested object/array +fields carry no typed flag and must be supplied as JSON through `--data`; +`--data -` reads the entire request body from stdin. Positional arguments and +explicitly-set typed flags override the matching keys inside `--data`. + +**Pages, components, sections** + +```bash +flashduty status-page list # List status pages (JSON: {"items":[...]}) +flashduty status-page info # Page detail, incl. component and section IDs +flashduty status-page create --name --url-name --type \ + --date-view --display-uptime-mode +flashduty status-page update [--name ] [--url-name ] ... # Update a page +flashduty status-page delete # Delete a page +flashduty status-page component-upsert --data '{"components":[{"name":"API","section_id":""}]}' +flashduty status-page component-delete [...] --page-id +flashduty status-page section-upsert --data '{"sections":[{"name":"Core"}]}' +flashduty status-page section-delete [...] --page-id +``` + +**Events (incident / maintenance) and their timeline** + +```bash +flashduty status-page change-active-list --type # Only in-progress events +flashduty status-page change-list --type --status +flashduty status-page change-info --page-id --change-id +flashduty status-page change-create --type --title \ + --status <status> --description <text> --data '{"updates":[...]}' +flashduty status-page change-update --page-id <page-id> --change-id <change-id> [--title <title>] +flashduty status-page change-delete --page-id <page-id> --change-id <change-id> +flashduty status-page change-timeline-create --page-id <page-id> --change-id <change-id> \ + --status <status> --description <text> [--data '{"component_changes":[...]}'] +flashduty status-page change-timeline-update --page-id <page-id> --change-id <change-id> --update-id <update-id> [--description <text>] +flashduty status-page change-timeline-delete --page-id <page-id> --change-id <change-id> --update-id <update-id> +``` + +`change-create` takes `<page-id>` as a **required positional argument**, and its +required `updates` array (with the nested `component_changes`) has no flag — so a +real `change-create` call always carries a `--data` payload: + +```bash +flashduty status-page change-create 5750613685214 --type incident \ + --title "API latency elevated" --status investigating \ + --description "Investigating elevated latency." \ + --data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"degraded"}]}]}' +``` + +The whole body can also come from stdin with `--data -`: + +```bash +cat change.json | flashduty status-page change-create 5750613685214 --data - +``` + +Resolving an incident goes through `change-timeline-create`; every component the +event touched must be moved back to `operational`: + +```bash +flashduty status-page change-timeline-create --page-id 5750613685214 --change-id 5821693893131 \ + --status resolved --description "Recovered." \ + --data '{"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"operational"}]}' +``` + +**Subscribers and templates** + +```bash +flashduty status-page subscriber-list <page-id> [--component-ids <ids>] [--page <n>] [--limit <n>] +flashduty status-page subscriber-import <page-id> --method <email|im> --data '{"subscribers":[...]}' +flashduty status-page subscriber-export <page-id> [--component-ids <ids>] +flashduty status-page template-list <page-id> --type <pre_defined|message> +flashduty status-page template-upsert <page-id> --type <pre_defined|message> --data '{"template":{...}}' +flashduty status-page template-delete --page-id <page-id> --template-id <template-id> --type <pre_defined|message> +``` + +**Migration from Atlassian Statuspage** ```bash -flashduty statuspage list [--id <ids>] # List status pages -flashduty statuspage changes --page-id <id> --type <incident|maintenance> # List active changes -flashduty statuspage create-incident --page-id <id> --title <title> # Create status incident -flashduty statuspage create-timeline --page-id <id> --change <id> --message <msg> # Add timeline update -flashduty statuspage migrate structure --from atlassian --source-page-id <id> --api-key <key> # Start structure/history migration -flashduty statuspage migrate email-subscribers --from atlassian --source-page-id <id> --target-page-id <id> --api-key <key> # Start email subscriber migration -flashduty statuspage migrate status --job-id <id> # Check migration job status -flashduty statuspage migrate cancel --job-id <id> # Cancel a running migration job +flashduty status-page migrate-structure <source-page-id> --api-key <key> [--url-name <slug>] # Structure + history +flashduty status-page migrate-email-subscribers --source-page-id <id> --target-page-id <id> --api-key <key> +flashduty status-page migration-status <job-id> # Check migration job status +flashduty status-page migration-cancel <job-id> # Cancel a running migration job ``` -Migration jobs are asynchronous. After starting `structure` or `email-subscribers`, use: +Migration jobs are asynchronous. After starting `migrate-structure` or +`migrate-email-subscribers`, poll the returned `job_id`: ```bash -flashduty statuspage migrate status --job-id <job_id> +flashduty status-page migration-status <job-id> ``` Typical flow: ```bash -flashduty statuspage migrate structure --from atlassian --source-page-id page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY -flashduty statuspage migrate status --job-id <structure_job_id> -flashduty statuspage migrate email-subscribers --from atlassian --source-page-id page_123 --target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY -flashduty statuspage migrate status --job-id <subscriber_job_id> +flashduty status-page migrate-structure page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY +flashduty status-page migration-status <structure_job_id> +flashduty status-page migrate-email-subscribers --source-page-id page_123 \ + --target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY +flashduty status-page migration-status <subscriber_job_id> ``` ### `template` - Notification Template Management (4 commands) diff --git a/README_zh.md b/README_zh.md index 2d4e2c5..7cdc250 100644 --- a/README_zh.md +++ b/README_zh.md @@ -185,13 +185,102 @@ flashduty field list [flags] # 列出自定义字段定义 支持 `--name`。 -### `statuspage` - 状态页管理(4 个命令) +### `status-page` - 状态页管理(28 个命令) + +命令组名是 `status-page`(带连字符),不是 `statuspage`。嵌套对象、数组类字段没有 +对应的 flag,必须通过 `--data` 传 JSON;`--data -` 表示整个请求体从 stdin 读取。 +位置参数和显式设置的 flag 会覆盖 `--data` 里的同名字段。 + +**状态页、组件、分组** + +```bash +flashduty status-page list # 列出状态页(JSON 形如 {"items":[...]}) +flashduty status-page info <page-id> # 状态页详情,含组件 ID 和分组 ID +flashduty status-page create --name <name> --url-name <slug> --type <public|internal> \ + --date-view <calendar|list> --display-uptime-mode <chart_and_percentage|chart|none> +flashduty status-page update <page-id> [--name <name>] [--url-name <slug>] ... # 更新状态页 +flashduty status-page delete <page-id> # 删除状态页 +flashduty status-page component-upsert <page-id> --data '{"components":[{"name":"API","section_id":"<section-id>"}]}' +flashduty status-page component-delete <component-id> [<id2>...] --page-id <page-id> +flashduty status-page section-upsert <page-id> --data '{"sections":[{"name":"核心服务"}]}' +flashduty status-page section-delete <section-id> [<id2>...] --page-id <page-id> +``` + +**事件(故障 / 维护)与时间线** + +```bash +flashduty status-page change-active-list <page-id> --type <incident|maintenance> # 只列进行中的事件 +flashduty status-page change-list <page-id> --type <incident|maintenance> --status <status> +flashduty status-page change-info --page-id <page-id> --change-id <change-id> +flashduty status-page change-create <page-id> --type <incident|maintenance> --title <title> \ + --status <status> --description <text> --data '{"updates":[...]}' +flashduty status-page change-update --page-id <page-id> --change-id <change-id> [--title <title>] +flashduty status-page change-delete --page-id <page-id> --change-id <change-id> +flashduty status-page change-timeline-create --page-id <page-id> --change-id <change-id> \ + --status <status> --description <text> [--data '{"component_changes":[...]}'] +flashduty status-page change-timeline-update --page-id <page-id> --change-id <change-id> --update-id <update-id> [--description <text>] +flashduty status-page change-timeline-delete --page-id <page-id> --change-id <change-id> --update-id <update-id> +``` + +`change-create` 的 `<page-id>` 是**必填位置参数**;必填的 `updates` 数组(以及嵌套在里面的 +`component_changes`)没有对应的 flag,所以真实的 `change-create` 调用一定带 `--data`: + +```bash +flashduty status-page change-create 5750613685214 --type incident \ + --title "API 延迟升高" --status investigating \ + --description "正在排查延迟升高问题。" \ + --data '{"updates":[{"status":"investigating","description":"团队正在排查。","component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"degraded"}]}]}' +``` + +整个请求体也可以用 `--data -` 从 stdin 读: + +```bash +cat change.json | flashduty status-page change-create 5750613685214 --data - +``` + +关闭事件走 `change-timeline-create`,并且事件涉及的每个组件都要改回 `operational`: + +```bash +flashduty status-page change-timeline-create --page-id 5750613685214 --change-id 5821693893131 \ + --status resolved --description "已恢复。" \ + --data '{"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"operational"}]}' +``` + +**订阅者与模板** + +```bash +flashduty status-page subscriber-list <page-id> [--component-ids <ids>] [--page <n>] [--limit <n>] +flashduty status-page subscriber-import <page-id> --method <email|im> --data '{"subscribers":[...]}' +flashduty status-page subscriber-export <page-id> [--component-ids <ids>] +flashduty status-page template-list <page-id> --type <pre_defined|message> +flashduty status-page template-upsert <page-id> --type <pre_defined|message> --data '{"template":{...}}' +flashduty status-page template-delete --page-id <page-id> --template-id <template-id> --type <pre_defined|message> +``` + +**从 Atlassian Statuspage 迁移** + +```bash +flashduty status-page migrate-structure <source-page-id> --api-key <key> [--url-name <slug>] # 迁移结构与历史 +flashduty status-page migrate-email-subscribers --source-page-id <id> --target-page-id <id> --api-key <key> +flashduty status-page migration-status <job-id> # 查询迁移任务状态 +flashduty status-page migration-cancel <job-id> # 取消正在跑的迁移任务 +``` + +迁移任务是异步的。启动 `migrate-structure` 或 `migrate-email-subscribers` 之后, +用返回的 `job_id` 轮询: + +```bash +flashduty status-page migration-status <job-id> +``` + +典型流程: ```bash -flashduty statuspage list [--id <ids>] # 列出状态页 -flashduty statuspage changes --page-id <id> --type <incident|maintenance> # 列出活跃的变更 -flashduty statuspage create-incident --page-id <id> --title <title> # 创建状态页事件 -flashduty statuspage create-timeline --page-id <id> --change <id> --message <msg> # 添加时间线更新 +flashduty status-page migrate-structure page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY +flashduty status-page migration-status <structure_job_id> +flashduty status-page migrate-email-subscribers --source-page-id page_123 \ + --target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY +flashduty status-page migration-status <subscriber_job_id> ``` ### `template` - 通知模板管理(4 个命令) diff --git a/e2e/auth_global_test.go b/e2e/auth_global_test.go index 5b4904b..4680a72 100644 --- a/e2e/auth_global_test.go +++ b/e2e/auth_global_test.go @@ -58,7 +58,7 @@ func TestHelpForEverySubcommand(t *testing.T) { // Test 110: all top-level commands show help without errors commands := []string{ "channel", "member", "team", "field", "escalation-rule", - "statuspage", "template", "change", "config", "login", + "status-page", "template", "change", "config", "login", } for _, cmd := range commands { t.Run(cmd, func(t *testing.T) { diff --git a/e2e/edge_case_test.go b/e2e/edge_case_test.go index ad9d4cf..8335fa3 100644 --- a/e2e/edge_case_test.go +++ b/e2e/edge_case_test.go @@ -49,7 +49,7 @@ func TestNoTruncOnFieldList(t *testing.T) { } func TestNoTruncOnStatusPageList(t *testing.T) { - r := runCLI(t, "statuspage", "list", "--no-trunc") + r := runCLI(t, "status-page", "list", "--no-trunc") requireSuccess(t, r) } diff --git a/e2e/resource_list_test.go b/e2e/resource_list_test.go index ecda3db..6d8329d 100644 --- a/e2e/resource_list_test.go +++ b/e2e/resource_list_test.go @@ -3,6 +3,7 @@ package e2e_test import ( + "strings" "testing" ) @@ -113,16 +114,25 @@ func TestChangeListJSON(t *testing.T) { // StatusPage // --------------------------------------------------------------------------- -// Test 248: statuspage list +// Test 248: status-page list +// +// `status-page list` is a generated command with no displayColumns entry, so the +// table columns are the reflective heuristic: the first 8 scalar fields of +// StatusPageItem, headed by their upper-cased JSON tag. Components/sections are +// nested arrays and are skipped by that heuristic, so there is no COMPONENTS +// column, and PAGE_ID/NAME/URL_NAME fall past the 8-column cut. func TestStatusPageList(t *testing.T) { - r := runCLI(t, "statuspage", "list") + r := runCLI(t, "status-page", "list") requireSuccess(t, r) - requireTableHeaders(t, r.Stdout, "ID", "NAME", "SLUG", "STATUS", "COMPONENTS") + if strings.HasPrefix(strings.TrimSpace(r.Stdout), "No results.") { + t.Skip("no status pages available") + } + requireTableHeaders(t, r.Stdout, "CONTACT_INFO", "CUSTOM_DOMAIN", "DATE_VIEW", "DISPLAY_UPTIME_MODE") } -// Test 252: statuspage list JSON +// Test 252: status-page list JSON func TestStatusPageListJSON(t *testing.T) { - r := runCLI(t, "statuspage", "list", "--json") + r := runCLI(t, "status-page", "list", "--json") requireSuccess(t, r) requireValidJSON(t, r.Stdout) } diff --git a/e2e/statuspage_test.go b/e2e/statuspage_test.go index d30cf67..af9db1f 100644 --- a/e2e/statuspage_test.go +++ b/e2e/statuspage_test.go @@ -9,14 +9,35 @@ import ( "testing" ) +// The CLI group is `status-page` (hyphenated) and its verbs come straight from +// the API paths — `change-active-list`, `change-create`, +// `change-timeline-create`, … — see internal/cli/zz_generated_status_pages.go. +// `statuspage`, `create-incident` and `create-timeline` never existed on this +// command tree. +// +// Generated list commands print their response envelope verbatim, so `--json` +// output is {"items":[...]} — an object, NOT a top-level array. + +// statusPageItems unmarshals the {"items":[...]} envelope of a status-page list +// response. +func statusPageItems(stdout string) ([]map[string]any, error) { + var envelope struct { + Items []map[string]any `json:"items"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &envelope); err != nil { + return nil, err + } + return envelope.Items, nil +} + // getFirstStatusPageID gets the first status page ID from the API. func getFirstStatusPageID(t *testing.T) string { t.Helper() - r := runCLI(t, "statuspage", "list", "--json") + r := runCLI(t, "status-page", "list", "--json") requireSuccess(t, r) - var pages []map[string]any - if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &pages); err != nil { - t.Skipf("could not parse statuspage list JSON: %v", err) + pages, err := statusPageItems(r.Stdout) + if err != nil { + t.Skipf("could not parse status-page list JSON: %v", err) } if len(pages) == 0 { t.Skip("no status pages available") @@ -46,53 +67,51 @@ func stringifyNumericID(t *testing.T, value any, field string) string { } } +// getStatusPageChanges lists the in-progress events of one type for a page. +// `page-id` is POSITIONAL on change-active-list; only `--type` is a flag. func getStatusPageChanges(t *testing.T, pageID, changeType string) []map[string]any { t.Helper() - r := runCLI(t, "statuspage", "changes", "--page-id", pageID, "--type", changeType, "--json") + r := runCLI(t, "status-page", "change-active-list", pageID, "--type", changeType, "--json") requireSuccess(t, r) requireValidJSON(t, r.Stdout) - var changes []map[string]any - if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &changes); err != nil { - t.Fatalf("could not parse statuspage changes JSON: %v\n%s", err, r.Stdout) + changes, err := statusPageItems(r.Stdout) + if err != nil { + t.Fatalf("could not parse status-page change-active-list JSON: %v\n%s", err, r.Stdout) } if len(changes) == 0 { - t.Logf("no %s statuspage changes returned for page_id=%s", changeType, pageID) + t.Logf("no active %s events returned for page_id=%s", changeType, pageID) } return changes } -// Test 249: statuspage list --id filter -func TestStatusPageListByID(t *testing.T) { +// Test 249: status-page info <page-id> returns the requested page. +// There is no `--id` filter on `list` (its only flag is `--data`), so a +// single-page lookup goes through `info`, whose response is a bare object. +func TestStatusPageInfoByID(t *testing.T) { id := getFirstStatusPageID(t) - r := runCLI(t, "statuspage", "list", "--id", id, "--json") + r := runCLI(t, "status-page", "info", id, "--json") requireSuccess(t, r) requireValidJSON(t, r.Stdout) - var pages []map[string]any - if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &pages); err != nil { - t.Fatalf("could not parse statuspage list JSON: %v\n%s", err, r.Stdout) - } - if len(pages) == 0 { - t.Skipf("statuspage list returned no rows for known page_id=%s", id) + var page map[string]any + if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &page); err != nil { + t.Fatalf("could not parse status-page info JSON: %v\n%s", err, r.Stdout) } - for _, page := range pages { - pageID := stringifyNumericID(t, page["page_id"], "page_id") - if pageID != id { - t.Fatalf("expected page_id=%s, got %s", id, pageID) - } + if got := stringifyNumericID(t, page["page_id"], "page_id"); got != id { + t.Fatalf("expected page_id=%s, got %s", id, got) } } -// Test 251: statuspage list --id invalid -func TestStatusPageListByIDInvalid(t *testing.T) { - r := runCLI(t, "statuspage", "list", "--id", "abc") +// Test 251: a non-numeric positional page id is rejected before any request. +func TestStatusPageChangeActiveListInvalidPageID(t *testing.T) { + r := runCLI(t, "status-page", "change-active-list", "abc", "--type", "incident") requireFailure(t, r) - requireContains(t, r.Stderr, "invalid --id") + requireContains(t, r.Stderr, "invalid page_id") } -// Test 253: statuspage changes --type incident +// Test 253: change-active-list --type incident func TestStatusPageChangesIncident(t *testing.T) { id := getFirstStatusPageID(t) changes := getStatusPageChanges(t, id, "incident") @@ -106,7 +125,7 @@ func TestStatusPageChangesIncident(t *testing.T) { } } -// Test 254: statuspage changes --type maintenance +// Test 254: change-active-list --type maintenance func TestStatusPageChangesMaintenance(t *testing.T) { id := getFirstStatusPageID(t) changes := getStatusPageChanges(t, id, "maintenance") @@ -120,20 +139,22 @@ func TestStatusPageChangesMaintenance(t *testing.T) { } } -// Test 255: statuspage changes missing --page-id +// Test 255: change-active-list without the <page-id> positional func TestStatusPageChangesMissingPageID(t *testing.T) { - r := runCLI(t, "statuspage", "changes", "--type", "incident") + r := runCLI(t, "status-page", "change-active-list", "--type", "incident") requireFailure(t, r) + requireContains(t, r.Stderr, "missing page_id") } -// Test 256: statuspage changes missing --type +// Test 256: change-active-list missing --type func TestStatusPageChangesMissingType(t *testing.T) { id := getFirstStatusPageID(t) - r := runCLI(t, "statuspage", "changes", "--page-id", id) + r := runCLI(t, "status-page", "change-active-list", id) requireFailure(t, r) + requireContains(t, r.Stderr, "missing required request fields: type") } -// Test 257: statuspage changes JSON +// Test 257: change-active-list JSON func TestStatusPageChangesJSON(t *testing.T) { id := getFirstStatusPageID(t) changes := getStatusPageChanges(t, id, "incident") @@ -147,32 +168,53 @@ func TestStatusPageChangesJSON(t *testing.T) { } } -// Test 261: statuspage create-incident missing --page-id -func TestStatusPageCreateIncidentMissingPageID(t *testing.T) { - r := runCLI(t, "statuspage", "create-incident", "--title", "test") +// Test 261: change-create without the <page-id> positional (and without the +// --page-id / --data fallbacks) fails in the argument validator. +func TestStatusPageChangeCreateMissingPageID(t *testing.T) { + r := runCLI(t, "status-page", "change-create", "--title", "test") + requireFailure(t, r) + requireContains(t, r.Stderr, "missing page_id") +} + +// Test 262: change-create with only the positional reports every missing +// required body field. +func TestStatusPageChangeCreateMissingTitle(t *testing.T) { + r := runCLI(t, "status-page", "change-create", "1", "--type", "incident", "--status", "investigating") requireFailure(t, r) + requireContains(t, r.Stderr, "title") } -// Test 262: statuspage create-incident missing --title -func TestStatusPageCreateIncidentMissingTitle(t *testing.T) { - r := runCLI(t, "statuspage", "create-incident", "--page-id", "1") +// Test 263: `updates` (and the nested `component_changes`) has NO typed flag, so +// a change-create that sets every scalar flag still fails until --data supplies +// it. This is the invariant that makes --data mandatory on real calls. +func TestStatusPageChangeCreateRequiresUpdatesViaData(t *testing.T) { + r := runCLI(t, "status-page", "change-create", "1", + "--type", "incident", "--status", "investigating", + "--title", "e2e", "--description", "e2e") requireFailure(t, r) + requireContains(t, r.Stderr, "missing required request fields: updates") } -// Test 267: statuspage create-timeline missing --page-id -func TestStatusPageCreateTimelineMissingPageID(t *testing.T) { - r := runCLI(t, "statuspage", "create-timeline", "--change", "1", "--message", "test") +// Test 267: change-timeline-create missing --page-id (both IDs are flags here, +// this command has no positional). +func TestStatusPageChangeTimelineCreateMissingPageID(t *testing.T) { + r := runCLI(t, "status-page", "change-timeline-create", "--change-id", "1", "--status", "investigating", "--description", "test") requireFailure(t, r) + requireContains(t, r.Stderr, "missing required request fields: page_id") } -// Test 268: statuspage create-timeline missing --change -func TestStatusPageCreateTimelineMissingChange(t *testing.T) { - r := runCLI(t, "statuspage", "create-timeline", "--page-id", "1", "--message", "test") +// Test 268: change-timeline-create missing --change-id +func TestStatusPageChangeTimelineCreateMissingChangeID(t *testing.T) { + r := runCLI(t, "status-page", "change-timeline-create", "--page-id", "1", "--status", "investigating", "--description", "test") requireFailure(t, r) + requireContains(t, r.Stderr, "missing required request fields: change_id") } -// Test 269: statuspage create-timeline missing --message -func TestStatusPageCreateTimelineMissingMessage(t *testing.T) { - r := runCLI(t, "statuspage", "create-timeline", "--page-id", "1", "--change", "1") +// Test 269: change-timeline-create missing --status. +// (--description is `omitempty` in the SDK request, so omitting it is caught by +// the server, not locally; --status is the locally-validated required field.) +func TestStatusPageChangeTimelineCreateMissingStatus(t *testing.T) { + r := runCLI(t, "status-page", "change-timeline-create", "--page-id", "1", "--change-id", "1", "--description", "test") requireFailure(t, r) + requireContains(t, r.Stderr, "missing required request fields: status") }