diff --git a/internal/output/spinner_test.go b/internal/output/spinner_test.go index a54596ce6..c4e683fae 100644 --- a/internal/output/spinner_test.go +++ b/internal/output/spinner_test.go @@ -9,6 +9,7 @@ import ( "testing" ) +// TestStartSpinner_DisabledIsNoop asserts that a disabled spinner writes nothing and its stop func is idempotent. func TestStartSpinner_DisabledIsNoop(t *testing.T) { var buf bytes.Buffer stop := StartSpinner(&buf, false, "working") @@ -19,11 +20,13 @@ func TestStartSpinner_DisabledIsNoop(t *testing.T) { } } +// TestStartSpinner_NilWriterIsNoop asserts that a nil writer is a no-op and stopping does not panic. func TestStartSpinner_NilWriterIsNoop(t *testing.T) { stop := StartSpinner(nil, true, "working") stop() // must not panic } +// TestStartSpinner_EnabledAnimatesAndCleansUp asserts that an enabled spinner renders a frame and label, then clears the line and restores the cursor on stop. func TestStartSpinner_EnabledAnimatesAndCleansUp(t *testing.T) { var buf bytes.Buffer stop := StartSpinner(&buf, true, "Publishing") diff --git a/shortcuts/apps/apps_db_audit_test.go b/shortcuts/apps/apps_db_audit_test.go index 832a5c9d4..becf9b86f 100644 --- a/shortcuts/apps/apps_db_audit_test.go +++ b/shortcuts/apps/apps_db_audit_test.go @@ -23,6 +23,7 @@ const ( // ── audit-status ── +// TestAppsDBAuditStatus_SingleTableObjectWithPlaceholder 验证单表查询无记录时返回 enabled:false 的占位对象(非数组)。 func TestAppsDBAuditStatus_SingleTableObjectWithPlaceholder(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -48,6 +49,7 @@ func TestAppsDBAuditStatus_SingleTableObjectWithPlaceholder(t *testing.T) { } } +// TestAppsDBAuditStatus_MultiTablePrettyTable 验证多表 pretty 输出含 enabled/yes/no 列与 retention 值。 func TestAppsDBAuditStatus_MultiTablePrettyTable(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -69,6 +71,7 @@ func TestAppsDBAuditStatus_MultiTablePrettyTable(t *testing.T) { // ── audit-enable / disable ── +// TestAppsDBAuditEnable_RequiresTableAndValidRetention 验证缺 --table 报必填错、非法 --retention 报 ValidationError。 func TestAppsDBAuditEnable_RequiresTableAndValidRetention(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) // 缺 --table → cobra required, exit 1 @@ -89,6 +92,7 @@ func TestAppsDBAuditEnable_RequiresTableAndValidRetention(t *testing.T) { } } +// TestAppsDBAuditEnable_DryRunAndSuccess 验证 dry-run 发出 enabled:true+retention 的 POST,成功时打印 pretty 确认行。 func TestAppsDBAuditEnable_DryRunAndSuccess(t *testing.T) { // dry-run body {table, enabled:true, retention} factory, stdout, _ := newAppsExecuteFactory(t) @@ -124,6 +128,7 @@ func TestAppsDBAuditEnable_DryRunAndSuccess(t *testing.T) { } } +// TestAppsDBAuditDisable_DryRunAndSuccess 验证 dry-run 发出 enabled:false 的 POST,成功时打印 pretty 确认行。 func TestAppsDBAuditDisable_DryRunAndSuccess(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBAuditDisable, @@ -156,6 +161,7 @@ func TestAppsDBAuditDisable_DryRunAndSuccess(t *testing.T) { // ── audit-list ── +// TestAppsDBAuditList_RequiresTable 验证缺 --table 时报必填错误。 func TestAppsDBAuditList_RequiresTable(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBAuditList, @@ -164,6 +170,7 @@ func TestAppsDBAuditList_RequiresTable(t *testing.T) { } } +// TestAppsDBAuditList_DryRunJoinsTables 验证 dry-run 将多个 --table 合并为 tables=orders,users 且归一化 since。 func TestAppsDBAuditList_DryRunJoinsTables(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBAuditList, @@ -188,6 +195,7 @@ func TestAppsDBAuditList_DryRunJoinsTables(t *testing.T) { } // 单表查询:不预过滤、直接打 audit_list(后端就 not-found/not-enabled 报错),无 skipped。 +// TestAppsDBAuditList_SingleTableNoPreflight 验证单表查询不预过滤、operator/before/after 还原为对象、无 skipped。 func TestAppsDBAuditList_SingleTableNoPreflight(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -220,6 +228,7 @@ func TestAppsDBAuditList_SingleTableNoPreflight(t *testing.T) { } } +// TestAppsDBAuditList_SingleTableEmptyPretty 验证单表无事件时不报错、pretty 打印 "No audit events found." 且无 Skipped。 func TestAppsDBAuditList_SingleTableEmptyPretty(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -238,6 +247,7 @@ func TestAppsDBAuditList_SingleTableEmptyPretty(t *testing.T) { // 多表查询:CLI 用 schema(存在性)+ status(审计开关)预过滤,只把有效表传给 audit_list, // 不存在 / 未开启审计的表进 skipped。 +// TestAppsDBAuditList_MultiTablePreflightFilters 验证多表查询用 schema+status 预过滤,仅传有效表,不存在/未开审计的表进 skipped。 func TestAppsDBAuditList_MultiTablePreflightFilters(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) // schema:orders/users/carts 存在,ghost 不存在。 @@ -281,6 +291,7 @@ func TestAppsDBAuditList_MultiTablePreflightFilters(t *testing.T) { } // 多表查询且全部被过滤掉 → 不调 audit_list,直接空 + skipped 提示。 +// TestAppsDBAuditList_MultiTableAllFilteredSkipsQuery 验证多表全部被过滤时跳过 audit_list 调用,直接输出空结果加 Skipped 提示。 func TestAppsDBAuditList_MultiTableAllFilteredSkipsQuery(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/apps/apps_db_changelog_list_test.go b/shortcuts/apps/apps_db_changelog_list_test.go index d85d3538e..af56ac51e 100644 --- a/shortcuts/apps/apps_db_changelog_list_test.go +++ b/shortcuts/apps/apps_db_changelog_list_test.go @@ -15,6 +15,7 @@ import ( const dbChangelogURL = "/open-apis/spark/v1/apps/app_x/db/changelog_list" +// TestAppsDBChangelogList_RequiresAppID 验证空白 --app-id 报 --app-id 的 ValidationError。 func TestAppsDBChangelogList_RequiresAppID(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsDBChangelogList, @@ -28,6 +29,7 @@ func TestAppsDBChangelogList_RequiresAppID(t *testing.T) { } } +// TestAppsDBChangelogList_DryRunFiltersAndTimeNormalize 验证 dry-run 透传 env/table/change_id 过滤参数并将 since 归一化为 RFC3339 UTC。 func TestAppsDBChangelogList_DryRunFiltersAndTimeNormalize(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBChangelogList, @@ -55,6 +57,7 @@ func TestAppsDBChangelogList_DryRunFiltersAndTimeNormalize(t *testing.T) { } } +// TestAppsDBChangelogList_RejectsBadSince 验证不可解析的 --since 报 --since 的 ValidationError。 func TestAppsDBChangelogList_RejectsBadSince(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsDBChangelogList, @@ -68,6 +71,7 @@ func TestAppsDBChangelogList_RejectsBadSince(t *testing.T) { } } +// TestAppsDBChangelogList_SuccessParsesOperator 验证成功响应中 operator JSON 串被解析为对象并输出变更字段。 func TestAppsDBChangelogList_SuccessParsesOperator(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -93,6 +97,7 @@ func TestAppsDBChangelogList_SuccessParsesOperator(t *testing.T) { } } +// TestAppsDBChangelogList_ChangeIDNotFoundPretty 验证按 --change-id 查询无结果时 pretty 打印 not-found 提示。 func TestAppsDBChangelogList_ChangeIDNotFoundPretty(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -108,6 +113,7 @@ func TestAppsDBChangelogList_ChangeIDNotFoundPretty(t *testing.T) { } } +// TestParseOperator_Cases 验证 parseOperator 处理合法 JSON、空 name 回退 id、非 JSON 原样、空串返回 nil,以及 operatorName(nil) 为占位符。 func TestParseOperator_Cases(t *testing.T) { if op := parseOperator(`{"id":"1","name":"a"}`); op == nil || op.ID != "1" || op.Name != "a" { t.Fatalf("valid: %#v", op) @@ -126,6 +132,7 @@ func TestParseOperator_Cases(t *testing.T) { } } +// TestSafeParseJSON_Cases 验证 safeParseJSON 合法 JSON 解析为对象、非法 JSON 原样返回字符串。 func TestSafeParseJSON_Cases(t *testing.T) { if v := safeParseJSON(`{"a":1}`); v == nil { t.Fatalf("valid json → object") diff --git a/shortcuts/apps/apps_db_data_export_test.go b/shortcuts/apps/apps_db_data_export_test.go index c71559cc5..f2c9121ac 100644 --- a/shortcuts/apps/apps_db_data_export_test.go +++ b/shortcuts/apps/apps_db_data_export_test.go @@ -18,6 +18,7 @@ import ( const dbDataExportURL = "/open-apis/spark/v1/apps/app_x/db/data_export" const dbOrdersRecordsURL = "/open-apis/spark/v1/apps/app_x/tables/orders/records" +// TestAppsDBDataExport_RequiresTable 验证缺 --table 时报必填错误。 func TestAppsDBDataExport_RequiresTable(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) // 缺 --table → cobra required-flag, exit 1 @@ -28,6 +29,7 @@ func TestAppsDBDataExport_RequiresTable(t *testing.T) { } } +// TestAppsDBDataExport_RejectsBadLimit 验证越界 --limit(0/-1/5001)均报 --limit 的 ValidationError。 func TestAppsDBDataExport_RejectsBadLimit(t *testing.T) { for _, lim := range []string{"0", "-1", "5001"} { factory, stdout, _ := newAppsExecuteFactory(t) @@ -43,6 +45,7 @@ func TestAppsDBDataExport_RejectsBadLimit(t *testing.T) { } } +// TestAppsDBDataExport_RejectsBadOutputExtension 验证不支持的 --output 扩展名(.xml)报校验错误。 func TestAppsDBDataExport_RejectsBadOutputExtension(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsDBDataExport, @@ -54,6 +57,7 @@ func TestAppsDBDataExport_RejectsBadOutputExtension(t *testing.T) { } // dry-run:format 跟随 --output 扩展名;缺省 csv。 +// TestAppsDBDataExport_DryRunFormatFromOutput 验证 dry-run 的 format 参数跟随 --output 扩展名、缺省为 csv,并带 limit。 func TestAppsDBDataExport_DryRunFormatFromOutput(t *testing.T) { cases := []struct{ output, wantFmt string }{ {"", "csv"}, {"orders.csv", "csv"}, {"orders.json", "json"}, {"dump.sql", "sql"}, @@ -89,6 +93,7 @@ func TestAppsDBDataExport_DryRunFormatFromOutput(t *testing.T) { } // 成功:先查 records 列表 total 计行,再把原始字节落盘。 +// TestAppsDBDataExport_SuccessWritesFile 验证成功路径先查 records total 计行、再将导出原始字节落盘并输出 rows/format/table。 func TestAppsDBDataExport_SuccessWritesFile(t *testing.T) { dir := chdirTemp(t) factory, stdout, reg := newAppsExecuteFactory(t) @@ -119,6 +124,7 @@ func TestAppsDBDataExport_SuccessWritesFile(t *testing.T) { } // 行数取自 records total,且按 --limit 截顶(min(total, limit))。 +// TestAppsDBDataExport_RowsFromTotalCappedByLimit 验证行数取 records total 并按 --limit 截顶(total=10000、limit=100 → rows=100)。 func TestAppsDBDataExport_RowsFromTotalCappedByLimit(t *testing.T) { chdirTemp(t) factory, stdout, reg := newAppsExecuteFactory(t) @@ -140,6 +146,7 @@ func TestAppsDBDataExport_RowsFromTotalCappedByLimit(t *testing.T) { } // total 查询失败(records 列表报错)→ 回退按导出文件内容数行,不阻断导出。 +// TestAppsDBDataExport_FallsBackToFileCountWhenTotalUnavailable 验证 records total 查询失败时回退按导出文件内容数行,不阻断落盘。 func TestAppsDBDataExport_FallsBackToFileCountWhenTotalUnavailable(t *testing.T) { dir := chdirTemp(t) factory, stdout, reg := newAppsExecuteFactory(t) @@ -165,6 +172,7 @@ func TestAppsDBDataExport_FallsBackToFileCountWhenTotalUnavailable(t *testing.T) } // 业务错误:网关回 JSON 信封 {code,msg}(非原始字节)→ typed error,不落盘。 +// TestAppsDBDataExport_BusinessErrorEnvelope 验证响应为 JSON 错误信封(非原始字节)时返回 typed error 且不落盘。 func TestAppsDBDataExport_BusinessErrorEnvelope(t *testing.T) { chdirTemp(t) factory, stdout, reg := newAppsExecuteFactory(t) diff --git a/shortcuts/apps/apps_db_data_import_test.go b/shortcuts/apps/apps_db_data_import_test.go index bd2c20522..14d29290b 100644 --- a/shortcuts/apps/apps_db_data_import_test.go +++ b/shortcuts/apps/apps_db_data_import_test.go @@ -28,6 +28,7 @@ func chdirTemp(t *testing.T) string { return dir } +// TestAppsDBDataImport_RequiresAppID 验证空白 --app-id 报 --app-id 的 ValidationError。 func TestAppsDBDataImport_RequiresAppID(t *testing.T) { chdirTemp(t) _ = os.WriteFile("orders.csv", []byte("id\n1\n"), 0o600) @@ -43,6 +44,7 @@ func TestAppsDBDataImport_RequiresAppID(t *testing.T) { } } +// TestAppsDBDataImport_RejectsUnsupportedFormat 验证非 csv/json 文件(.txt)报不支持格式的校验错误。 func TestAppsDBDataImport_RejectsUnsupportedFormat(t *testing.T) { chdirTemp(t) _ = os.WriteFile("data.txt", []byte("x\n"), 0o600) @@ -55,6 +57,7 @@ func TestAppsDBDataImport_RejectsUnsupportedFormat(t *testing.T) { } } +// TestAppsDBDataImport_RequiresConfirmation 验证缺 --yes 时报 requires confirmation 错误。 func TestAppsDBDataImport_RequiresConfirmation(t *testing.T) { chdirTemp(t) _ = os.WriteFile("orders.csv", []byte("id\n1\n"), 0o600) @@ -66,6 +69,7 @@ func TestAppsDBDataImport_RequiresConfirmation(t *testing.T) { } } +// TestAppsDBDataImport_RejectsOversizeFile 验证超过 1MB 上限的文件报 --file 的 ValidationError。 func TestAppsDBDataImport_RejectsOversizeFile(t *testing.T) { chdirTemp(t) // >1MB → size 校验 @@ -84,6 +88,7 @@ func TestAppsDBDataImport_RejectsOversizeFile(t *testing.T) { } // dry-run:multipart 上传——file_name + file 走 body,env + table 走 query(table 缺省取文件名)。 +// TestAppsDBDataImport_DryRunMultipartShape 验证 dry-run 的 multipart 形态:file_name+file 走 body、env+table 走 query 且不再发 format。 func TestAppsDBDataImport_DryRunMultipartShape(t *testing.T) { chdirTemp(t) _ = os.WriteFile("orders.csv", []byte("id\n1\n"), 0o600) @@ -116,6 +121,7 @@ func TestAppsDBDataImport_DryRunMultipartShape(t *testing.T) { } } +// TestAppsDBDataImport_Success 验证成功导入后输出含 table、rows 与回显的 file 名。 func TestAppsDBDataImport_Success(t *testing.T) { chdirTemp(t) _ = os.WriteFile("orders.csv", []byte("id,name\n1,a\n2,b\n"), 0o600) @@ -134,6 +140,7 @@ func TestAppsDBDataImport_Success(t *testing.T) { } } +// TestAppsDBDataImport_TableDefaultsToFileBasename 验证未传 --table 时表名缺省取文件名去扩展名(customers.json→customers)。 func TestAppsDBDataImport_TableDefaultsToFileBasename(t *testing.T) { chdirTemp(t) _ = os.WriteFile("customers.json", []byte(`[{"id":1}]`), 0o600) diff --git a/shortcuts/apps/apps_db_env_recovery_quota_test.go b/shortcuts/apps/apps_db_env_recovery_quota_test.go index e8b227e95..78722d693 100644 --- a/shortcuts/apps/apps_db_env_recovery_quota_test.go +++ b/shortcuts/apps/apps_db_env_recovery_quota_test.go @@ -23,6 +23,7 @@ const ( // ── env-diff ── +// TestAppsDBEnvDiff_DryRunBody 校验 dry-run 请求体:POST env_migrate 且 dry_run=true。 func TestAppsDBEnvDiff_DryRunBody(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBEnvDiff, @@ -43,6 +44,7 @@ func TestAppsDBEnvDiff_DryRunBody(t *testing.T) { } } +// TestAppsDBEnvDiff_SuccessRendersChanges 验证 pretty 输出渲染出 dev → online 变更摘要及 DDL 语句。 func TestAppsDBEnvDiff_SuccessRendersChanges(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -64,6 +66,7 @@ func TestAppsDBEnvDiff_SuccessRendersChanges(t *testing.T) { } } +// TestAppsDBEnvDiff_EmptyChanges 验证无变更时 pretty 输出"无待发布变更"提示。 func TestAppsDBEnvDiff_EmptyChanges(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -81,6 +84,7 @@ func TestAppsDBEnvDiff_EmptyChanges(t *testing.T) { // ── env-migrate ── +// TestAppsDBEnvMigrate_DryRunBody 校验 migrate 的 dry-run 请求体里 dry_run=false(真实迁移)。 func TestAppsDBEnvMigrate_DryRunBody(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBEnvMigrate, @@ -119,6 +123,7 @@ func TestAppsDBEnvMigrate_AsyncPollSuccess(t *testing.T) { } } +// TestAppsDBEnvMigrate_PollFailedSurfacesError 验证轮询到 failed 时返回 API/server_error 类型错误,携带服务端 message 与恢复 hint。 func TestAppsDBEnvMigrate_PollFailedSurfacesError(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -143,6 +148,7 @@ func TestAppsDBEnvMigrate_PollFailedSurfacesError(t *testing.T) { } } +// TestAppsDBEnvMigrate_RequiresConfirmation 验证 high-risk-write 无 --yes 时被确认门拦截。 func TestAppsDBEnvMigrate_RequiresConfirmation(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) // high-risk-write 无 --yes → 应被确认门拦截(非 0 退出)。 @@ -154,6 +160,7 @@ func TestAppsDBEnvMigrate_RequiresConfirmation(t *testing.T) { // ── recovery-diff ── +// TestAppsDBRecoveryDiff_RequiresTarget 验证缺少 --target 时报必填错误。 func TestAppsDBRecoveryDiff_RequiresTarget(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBRecoveryDiff, @@ -162,6 +169,7 @@ func TestAppsDBRecoveryDiff_RequiresTarget(t *testing.T) { } } +// TestAppsDBRecoveryDiff_DryRunNormalizesTarget 验证 dry-run 走 POST env_recovery 且 --target 被归一化为 RFC3339 UTC。 func TestAppsDBRecoveryDiff_DryRunNormalizesTarget(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBRecoveryDiff, @@ -185,6 +193,7 @@ func TestAppsDBRecoveryDiff_DryRunNormalizesTarget(t *testing.T) { } } +// TestAppsDBRecoveryDiff_SuccessRendersChanges 验证 preview 成功后 pretty 渲染受影响表数、行增删与预估耗时。 func TestAppsDBRecoveryDiff_SuccessRendersChanges(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -213,6 +222,7 @@ func TestAppsDBRecoveryDiff_SuccessRendersChanges(t *testing.T) { } } +// TestAppsDBRecoveryDiff_PreviewFailed 验证 preview_status=failed 时返回 API/server_error,携带 message 与 PITR window hint。 func TestAppsDBRecoveryDiff_PreviewFailed(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -239,6 +249,7 @@ func TestAppsDBRecoveryDiff_PreviewFailed(t *testing.T) { // ── recovery-apply ── +// TestAppsDBRecoveryApply_NoChangesShortCircuits 验证 status=no_changes 时短路输出"已是该状态",不再轮询。 func TestAppsDBRecoveryApply_NoChangesShortCircuits(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -254,6 +265,7 @@ func TestAppsDBRecoveryApply_NoChangesShortCircuits(t *testing.T) { } } +// TestAppsDBRecoveryApply_AsyncPollSuccess 验证 running → 轮询 success 后 pretty 输出恢复完成及耗时。 func TestAppsDBRecoveryApply_AsyncPollSuccess(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -273,6 +285,7 @@ func TestAppsDBRecoveryApply_AsyncPollSuccess(t *testing.T) { } } +// TestAppsDBRecoveryApply_RequiresConfirmation 验证无 --yes 时被确认门拦截。 func TestAppsDBRecoveryApply_RequiresConfirmation(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBRecoveryApply, @@ -283,6 +296,7 @@ func TestAppsDBRecoveryApply_RequiresConfirmation(t *testing.T) { // ── quota-get ── +// TestAppsDBQuotaGet_WithQuotaPretty 验证已对接配额时 pretty 渲染存储用量、百分比及 tables/views 数。 func TestAppsDBQuotaGet_WithQuotaPretty(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/apps/apps_db_execute.go b/shortcuts/apps/apps_db_execute.go index a68351f8d..7503fe0fa 100644 --- a/shortcuts/apps/apps_db_execute.go +++ b/shortcuts/apps/apps_db_execute.go @@ -547,6 +547,7 @@ func isDMLType(sqlType string) bool { return false } +// dmlVerb 把 DML sql_type 映射成过去分词动词:INSERT→inserted / UPDATE→updated / DELETE→deleted / MERGE→merged,未知 → affected。 func dmlVerb(sqlType string) string { switch strings.ToUpper(sqlType) { case "INSERT": @@ -561,6 +562,7 @@ func dmlVerb(sqlType string) string { return "affected" } +// plural 返回英文复数后缀:n==1 时空串,否则 "s"。 func plural(n int64) string { if n == 1 { return "" diff --git a/shortcuts/apps/apps_db_execute_test.go b/shortcuts/apps/apps_db_execute_test.go index 25866bdcc..335e27e38 100644 --- a/shortcuts/apps/apps_db_execute_test.go +++ b/shortcuts/apps/apps_db_execute_test.go @@ -15,6 +15,7 @@ import ( "github.com/larksuite/cli/internal/output" ) +// TestAppsDBExecute_SingleSELECTJSONIsRowArray 断言单条 SELECT 的 JSON data 直接是行数组(不再透传 result 字符串)。 func TestAppsDBExecute_SingleSELECTJSONIsRowArray(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -48,6 +49,7 @@ func TestAppsDBExecute_SingleSELECTJSONIsRowArray(t *testing.T) { } } +// TestAppsDBExecute_SingleDMLJSONShape 断言单条 DML 的 JSON data 形如 {command, rows_affected}。 func TestAppsDBExecute_SingleDMLJSONShape(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -80,6 +82,7 @@ func TestAppsDBExecute_SingleDMLJSONShape(t *testing.T) { } } +// TestAppsDBExecute_SingleDDLJSONShape 断言单条 DDL 的 JSON data 形如 {command}。 func TestAppsDBExecute_SingleDDLJSONShape(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -111,6 +114,7 @@ func TestAppsDBExecute_SingleDDLJSONShape(t *testing.T) { } } +// TestAppsDBExecute_MultiStatementJSONShape 断言多语句的 JSON data 是元素数组,且 SELECT 包成 {command:"SELECT", rows:[...]}。 func TestAppsDBExecute_MultiStatementJSONShape(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -153,6 +157,7 @@ func TestAppsDBExecute_MultiStatementJSONShape(t *testing.T) { } } +// TestAppsDBExecute_DryRunSendsTransactionalFalse 断言 dry-run 发出的请求是 POST、params 带 transactional=false(DBA 模式)且 transactional 不在 body 里。 func TestAppsDBExecute_DryRunSendsTransactionalFalse(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsDBExecute, @@ -188,6 +193,7 @@ func TestAppsDBExecute_DryRunSendsTransactionalFalse(t *testing.T) { } } +// TestAppsDBExecute_RejectsEmptySQL 断言 --sql 全空白时校验报错(提示需要 --sql 或 --file)。 func TestAppsDBExecute_RejectsEmptySQL(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsDBExecute, @@ -250,6 +256,7 @@ func TestAppsDBExecute_FileReadsSQLIntoBody(t *testing.T) { // 输入用 BOE 真实抓包数据(test_scripts/boe_e2e/run.log)。 // ============================================================================ +// TestAppsDBExecute_LegacyWireSingleSelect 断言 legacy 字符串数组 wire 的单 SELECT 能正常渲染表格、不回退到 RAW。 func TestAppsDBExecute_LegacyWireSingleSelect(t *testing.T) { // BOE 实测:SELECT 1 AS x → result: "[\"[{\\\"x\\\":1}]\"]" factory, stdout, reg := newAppsExecuteFactory(t) @@ -281,6 +288,7 @@ func TestAppsDBExecute_LegacyWireSingleSelect(t *testing.T) { } } +// TestAppsDBExecute_LegacyWireSingleSelectJSONIsRowArray 断言 legacy wire 的 SELECT 同样归一化成 PRD 行数组形态。 func TestAppsDBExecute_LegacyWireSingleSelectJSONIsRowArray(t *testing.T) { // 验证 legacy wire 的 SELECT 也归一化成 PRD 行数组形态(data 直接是行) factory, stdout, reg := newAppsExecuteFactory(t) @@ -313,6 +321,7 @@ func TestAppsDBExecute_LegacyWireSingleSelectJSONIsRowArray(t *testing.T) { } } +// TestAppsDBExecute_LegacyWireMultiSelect 断言 legacy wire 多 SELECT 输出带 Statement N header 与末尾 "✓ N statements executed" 汇总。 func TestAppsDBExecute_LegacyWireMultiSelect(t *testing.T) { // BOE 实测:SELECT 1; SELECT 2 → result: "[\"[{\\\"?column?\\\":1}]\",\"[{\\\"?column?\\\":2}]\"]" factory, stdout, reg := newAppsExecuteFactory(t) @@ -342,6 +351,7 @@ func TestAppsDBExecute_LegacyWireMultiSelect(t *testing.T) { } } +// TestAppsDBExecute_LegacyWireDDLEmptyResult 断言 result 为空字符串时(legacy DDL)pretty 输出 "(empty result)"。 func TestAppsDBExecute_LegacyWireDDLEmptyResult(t *testing.T) { // BOE 实测:CREATE TABLE → result: "" (空字符串,无 rows) // 老 wire 不区分 DDL/DML/无返回,统一标 "ok" @@ -368,6 +378,7 @@ func TestAppsDBExecute_LegacyWireDDLEmptyResult(t *testing.T) { } } +// TestAppsDBExecute_LegacyWireMultiSelectWithRealTable 断言含 CJK / uuid / int 字段的真实表行能正确显示在 pretty 表格里。 func TestAppsDBExecute_LegacyWireMultiSelectWithRealTable(t *testing.T) { // BOE 实测真实表抓包(course 表第一行):复杂 JSON 含 CJK / timestamp / uuid 字段 factory, stdout, reg := newAppsExecuteFactory(t) @@ -426,6 +437,7 @@ func TestAppsDBExecute_PrettySingleSelectTable(t *testing.T) { } } +// TestAppsDBExecute_PrettyEmptySelect 断言空 SELECT 的 pretty 输出为 "(0 rows)"。 func TestAppsDBExecute_PrettyEmptySelect(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -448,6 +460,7 @@ func TestAppsDBExecute_PrettyEmptySelect(t *testing.T) { } } +// TestAppsDBExecute_PrettySingleDMLAndDDL 断言单条 DML 渲染 "✓ N row(s) "、各类 DDL(含细粒度动词)渲染 "✓ DDL executed"。 func TestAppsDBExecute_PrettySingleDMLAndDDL(t *testing.T) { cases := []struct { name string @@ -484,6 +497,7 @@ func TestAppsDBExecute_PrettySingleDMLAndDDL(t *testing.T) { } } +// TestAppsDBExecute_PrettyMultiStatementsAllSuccess 断言多语句全成功时逐条 Statement 摘要 + 末尾 "✓ N statements executed"。 func TestAppsDBExecute_PrettyMultiStatementsAllSuccess(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -553,6 +567,7 @@ func TestAppsDBExecute_PrettyMultiStatementsDDL(t *testing.T) { } } +// TestAppsDBExecute_PrettyMultiStatementsPartialFailureWithErrorSentinel 断言多语句部分失败时 pretty 仍打逐条 ✓/✗ 摘要、声明前序已 commit 未回滚,且返回 typed error、不打成功汇总。 func TestAppsDBExecute_PrettyMultiStatementsPartialFailureWithErrorSentinel(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -724,6 +739,7 @@ func TestAppsDBExecute_TransactionFailureRolledBack(t *testing.T) { } } +// TestInferRolledBack_Cases 断言 inferRolledBack 按 BEGIN/COMMIT/ROLLBACK 计数判定失败时事务是否仍开着(即整批回滚)。 func TestInferRolledBack_Cases(t *testing.T) { stmt := func(t string) map[string]interface{} { return map[string]interface{}{"sql_type": t} } cases := []struct { @@ -747,6 +763,7 @@ func TestInferRolledBack_Cases(t *testing.T) { } } +// TestCellString_AllKinds 断言 cellString 对 nil/string/bool/整数/小数/对象各类型的字符串化结果。 func TestCellString_AllKinds(t *testing.T) { cases := []struct { name string @@ -770,6 +787,7 @@ func TestCellString_AllKinds(t *testing.T) { } } +// TestCodeString_Forms 断言 codeString 处理 nil / "k_dl_xxx" / 纯数字串 / float64 / 不支持类型各形态。 func TestCodeString_Forms(t *testing.T) { cases := []struct { name string @@ -791,6 +809,7 @@ func TestCodeString_Forms(t *testing.T) { } } +// TestDmlVerb_AllVerbs 断言 dmlVerb 对 INSERT/UPDATE/DELETE/MERGE 的动词映射(大小写不敏感),非 DML 返回 affected。 func TestDmlVerb_AllVerbs(t *testing.T) { cases := map[string]string{ "INSERT": "inserted", @@ -806,6 +825,7 @@ func TestDmlVerb_AllVerbs(t *testing.T) { } } +// TestIntOrZero_Cases 断言 intOrZero 对 JSON number 取整、对非数字 / nil 返回 0。 func TestIntOrZero_Cases(t *testing.T) { if got := intOrZero(float64(5)); got != 5 { t.Errorf("intOrZero(5)=%d want 5", got) @@ -818,6 +838,7 @@ func TestIntOrZero_Cases(t *testing.T) { } } +// TestErrorSummary_Cases 断言 errorSummary 对空 / 非法 JSON / 带 code / 无 code 各情形生成 "message [code]" 文案。 func TestErrorSummary_Cases(t *testing.T) { cases := []struct { name, in, want string @@ -836,6 +857,7 @@ func TestErrorSummary_Cases(t *testing.T) { } } +// TestParseErrorSentinel_Cases 断言 parseErrorSentinel 解析 ERROR 哨兵 data 得到数值 code 与 message(含空 / 非法 / 空 message 回退)。 func TestParseErrorSentinel_Cases(t *testing.T) { cases := []struct { name, in string @@ -857,6 +879,7 @@ func TestParseErrorSentinel_Cases(t *testing.T) { } } +// TestIsStructuredResult_Cases 断言 isStructuredResult 仅在首元素含 sql_type 时判为新结构化形态。 func TestIsStructuredResult_Cases(t *testing.T) { if !isStructuredResult([]map[string]interface{}{{"sql_type": "SELECT"}}) { t.Error("expected structured=true when sql_type present") @@ -869,6 +892,7 @@ func TestIsStructuredResult_Cases(t *testing.T) { } } +// TestNormalizeLegacyStatement_Cases 断言 normalizeLegacyStatement 把空 / null / 非 JSON 标为 OK、把 rows 数组标为 SELECT 并带 record_count。 func TestNormalizeLegacyStatement_Cases(t *testing.T) { t.Run("empty -> OK", func(t *testing.T) { got := normalizeLegacyStatement("") @@ -899,6 +923,7 @@ func TestNormalizeLegacyStatement_Cases(t *testing.T) { }) } +// TestCellString_MarshalFallback 断言 cellString 对 json.Marshal 拒绝的类型(如 complex)回退到 fmt %v。 func TestCellString_MarshalFallback(t *testing.T) { // complex128 is not switch-handled and json.Marshal rejects it → // falls back to fmt.Sprintf("%v", v), which is deterministic for complex. @@ -907,6 +932,7 @@ func TestCellString_MarshalFallback(t *testing.T) { } } +// TestRenderSingleStatementPretty_Branches 断言 renderSingleStatementPretty 对 SELECT/ERROR/DML/legacy OK/DDL 各分支的输出。 func TestRenderSingleStatementPretty_Branches(t *testing.T) { cases := []struct { name string @@ -930,6 +956,7 @@ func TestRenderSingleStatementPretty_Branches(t *testing.T) { } } +// TestRenderSelectRowsAsTable_Branches 断言 renderSelectRowsAsTable 对空串 / 空数组 / 非法 JSON 回退 / 正常 rows 各分支的输出。 func TestRenderSelectRowsAsTable_Branches(t *testing.T) { cases := []struct { name string diff --git a/shortcuts/apps/apps_file_delete_test.go b/shortcuts/apps/apps_file_delete_test.go index f1c9bb541..edfce9241 100644 --- a/shortcuts/apps/apps_file_delete_test.go +++ b/shortcuts/apps/apps_file_delete_test.go @@ -15,6 +15,7 @@ import ( const fileDeleteURL = "/open-apis/spark/v1/apps/app_x/storage/file_batch_remove" +// TestAppsFileDelete_RequiresAppIDAndPath 验证仅含空白的 --path 去空后为空时,Validate 报 --path typed 校验错误。 func TestAppsFileDelete_RequiresAppIDAndPath(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) // 传入仅含空白的 --path:满足 cobra 的 Required 检查,但 cleanDeletePaths 去空后为空, @@ -40,6 +41,7 @@ func TestAppsFileDelete_RequiresConfirmation(t *testing.T) { } } +// TestAppsFileDelete_DryRunSendsPaths 验证 dry-run 输出 POST file_batch_remove,body.paths 按序携带多个 --path。 func TestAppsFileDelete_DryRunSendsPaths(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsFileDelete, @@ -105,6 +107,7 @@ func TestAppsFileDelete_PartialFailureStillOK(t *testing.T) { } } +// TestAppsFileDelete_PrettySummary 验证 pretty 输出逐项 ✓/✗ 标记并汇总 "1/2 deleted"。 func TestAppsFileDelete_PrettySummary(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/apps/apps_file_download_test.go b/shortcuts/apps/apps_file_download_test.go index 8bc1bcaa8..1d4297971 100644 --- a/shortcuts/apps/apps_file_download_test.go +++ b/shortcuts/apps/apps_file_download_test.go @@ -20,6 +20,7 @@ import ( const fileSignURLForDownload = "/open-apis/spark/v1/apps/app_x/storage/file_sign" +// TestAppsFileDownload_RequiresAppIDAndPath 验证仅含空白的 --path 触发 --path typed 校验错误。 func TestAppsFileDownload_RequiresAppIDAndPath(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsFileDownload, @@ -33,6 +34,7 @@ func TestAppsFileDownload_RequiresAppIDAndPath(t *testing.T) { } } +// TestAppsFileDownload_DryRunSignsFirst 验证 dry-run 第一步是 POST file_sign。 func TestAppsFileDownload_DryRunSignsFirst(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsFileDownload, diff --git a/shortcuts/apps/apps_file_get_test.go b/shortcuts/apps/apps_file_get_test.go index b48b4b45a..ec78811a2 100644 --- a/shortcuts/apps/apps_file_get_test.go +++ b/shortcuts/apps/apps_file_get_test.go @@ -15,6 +15,7 @@ import ( const fileGetURL = "/open-apis/spark/v1/apps/app_x/storage/file" +// TestAppsFileGet_RequiresAppIDAndPath 验证空白 --app-id 与空白 --path 分别触发对应的 typed 校验错误。 func TestAppsFileGet_RequiresAppIDAndPath(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsFileGet, @@ -38,6 +39,7 @@ func TestAppsFileGet_RequiresAppIDAndPath(t *testing.T) { } } +// TestAppsFileGet_DryRunSendsPathQuery 验证 dry-run 输出 GET file,path 作为 query 参数下发。 func TestAppsFileGet_DryRunSendsPathQuery(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsFileGet, @@ -57,6 +59,7 @@ func TestAppsFileGet_DryRunSendsPathQuery(t *testing.T) { } } +// TestAppsFileGet_SuccessAndPrettyKeyValue 验证 pretty key/value 展示 size 含 bytes、uploaded_by 只显示 name 且不泄漏 user id。 func TestAppsFileGet_SuccessAndPrettyKeyValue(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/apps/apps_file_list_test.go b/shortcuts/apps/apps_file_list_test.go index 3247e44c1..c6616e4ab 100644 --- a/shortcuts/apps/apps_file_list_test.go +++ b/shortcuts/apps/apps_file_list_test.go @@ -68,6 +68,7 @@ func TestNormalizeTimestamp_AllFormats(t *testing.T) { const fileListURL = "/open-apis/spark/v1/apps/app_x/storage/file_list" +// TestAppsFileList_RequiresAppID 验证空白 --app-id 触发 --app-id typed 校验错误。 func TestAppsFileList_RequiresAppID(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsFileList, @@ -197,6 +198,7 @@ func TestAppsFileList_SuccessProjectsCreatedToUploaded(t *testing.T) { } } +// TestAppsFileList_PrettyTableAndEmpty 验证 pretty 非空时渲染表头与人类可读 size,空结果时输出 "No files found."。 func TestAppsFileList_PrettyTableAndEmpty(t *testing.T) { // 非空:5 列表头。 factory, stdout, reg := newAppsExecuteFactory(t) @@ -233,6 +235,7 @@ func TestAppsFileList_PrettyTableAndEmpty(t *testing.T) { } } +// TestParseFileUser_Cases 验证 parseFileUser:合法 JSON 解析成对象,空串/非法/全空字段均返回 nil。 func TestParseFileUser_Cases(t *testing.T) { if u := parseFileUser(`{"id":"1","name":"a"}`); u == nil || u.ID != "1" || u.Name != "a" { t.Fatalf("valid parse failed: %#v", u) diff --git a/shortcuts/apps/apps_file_quota_get_test.go b/shortcuts/apps/apps_file_quota_get_test.go index 046434d38..b1062fecd 100644 --- a/shortcuts/apps/apps_file_quota_get_test.go +++ b/shortcuts/apps/apps_file_quota_get_test.go @@ -12,6 +12,7 @@ import ( const fileQuotaURL = "/open-apis/spark/v1/apps/app_x/storage/file_quota" +// TestAppsFileQuotaGet_QuotaConnectedShowsAllFields 验证配额已对接时输出 storage_quota_bytes/usage_percent/files 全字段。 func TestAppsFileQuotaGet_QuotaConnectedShowsAllFields(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -62,6 +63,7 @@ func TestAppsFileQuotaGet_UnconnectedOmitsQuotaFields(t *testing.T) { } } +// TestProjectFileQuota_DeletesZeroQuota 验证 projectFileQuota:quota=0 时删除 storage_quota_bytes/usage_percent,非零时保留。 func TestProjectFileQuota_DeletesZeroQuota(t *testing.T) { data := map[string]interface{}{"storage_used_bytes": 100, "storage_quota_bytes": float64(0), "usage_percent": float64(0), "files": 3} projectFileQuota(data) diff --git a/shortcuts/apps/apps_file_sign_test.go b/shortcuts/apps/apps_file_sign_test.go index 930666ae0..84ebbaa79 100644 --- a/shortcuts/apps/apps_file_sign_test.go +++ b/shortcuts/apps/apps_file_sign_test.go @@ -15,6 +15,7 @@ import ( const fileSignURL = "/open-apis/spark/v1/apps/app_x/storage/file_sign" +// TestAppsFileSign_DryRunBody 验证 dry-run 输出 POST file_sign,body 携带 path 与 expires_in。 func TestAppsFileSign_DryRunBody(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) if err := runAppsShortcut(t, AppsFileSign, @@ -38,6 +39,7 @@ func TestAppsFileSign_DryRunBody(t *testing.T) { } } +// TestAppsFileSign_RejectsDurationOverMax 验证 --expires-in 超过上限时触发 --expires-in typed 校验错误。 func TestAppsFileSign_RejectsDurationOverMax(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) err := runAppsShortcut(t, AppsFileSign, @@ -51,6 +53,7 @@ func TestAppsFileSign_RejectsDurationOverMax(t *testing.T) { } } +// TestAppsFileSign_PrettyPrintsSignedURL 验证 pretty 只输出 signed_url 本身。 func TestAppsFileSign_PrettyPrintsSignedURL(t *testing.T) { factory, stdout, reg := newAppsExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/apps/apps_file_upload_test.go b/shortcuts/apps/apps_file_upload_test.go index edd1f2723..3182afee6 100644 --- a/shortcuts/apps/apps_file_upload_test.go +++ b/shortcuts/apps/apps_file_upload_test.go @@ -18,6 +18,7 @@ import ( "github.com/larksuite/cli/internal/httpmock" ) +// TestAppsFileUpload_RequiresAppIDAndFile 验证仅含空白的 --file 经 Validate 去空后触发 --file typed 校验错误。 func TestAppsFileUpload_RequiresAppIDAndFile(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) // --file is a cobra-required flag; pass whitespace so cobra's required check @@ -33,6 +34,7 @@ func TestAppsFileUpload_RequiresAppIDAndFile(t *testing.T) { } } +// TestAppsFileUpload_RejectsDirectory 验证 --file 指向目录时触发 --file typed 校验错误。 func TestAppsFileUpload_RejectsDirectory(t *testing.T) { dir := t.TempDir() oldWD, _ := os.Getwd() @@ -55,6 +57,7 @@ func TestAppsFileUpload_RejectsDirectory(t *testing.T) { } } +// TestAppsFileUpload_DryRunPreUpload 验证 dry-run 输出 POST file_pre_upload,body.file_name 取文件 basename。 func TestAppsFileUpload_DryRunPreUpload(t *testing.T) { // Validate 会 Stat --file(在 DryRun 之前),故 dry-run 也需要真实存在的文件。 dir := t.TempDir() @@ -149,6 +152,7 @@ func TestAppsFileUpload_EndToEnd(t *testing.T) { } } +// TestSanitizeUploadFileName_Cases 验证 sanitizeUploadFileName:空格转 %20、去 TOS 非法字符、全非法兜底、非 ASCII 百分号编码。 func TestSanitizeUploadFileName_Cases(t *testing.T) { cases := []struct{ in, want string }{ {"logo.png", "logo.png"}, @@ -164,6 +168,7 @@ func TestSanitizeUploadFileName_Cases(t *testing.T) { } } +// TestMimeByExt_Cases 验证 mimeByExt:按扩展名识别 image/png,未知扩展名兜底 application/octet-stream。 func TestMimeByExt_Cases(t *testing.T) { if got := mimeByExt("a.png"); !strings.HasPrefix(got, "image/png") { t.Errorf("mimeByExt(a.png)=%q want image/png", got) diff --git a/shortcuts/apps/shortcuts_test.go b/shortcuts/apps/shortcuts_test.go index a6beb4a35..b60af1902 100644 --- a/shortcuts/apps/shortcuts_test.go +++ b/shortcuts/apps/shortcuts_test.go @@ -44,6 +44,7 @@ func TestAppsShortcuts_IncludesSessionCommands(t *testing.T) { } } +// TestAppsGitCredentialHelper_IsNotAShortcut 确认 git credential helper 不作为 shortcut 暴露。 func TestAppsGitCredentialHelper_IsNotAShortcut(t *testing.T) { for _, shortcut := range Shortcuts() { if shortcut.Command == "git-credential-helper" { @@ -52,18 +53,21 @@ func TestAppsGitCredentialHelper_IsNotAShortcut(t *testing.T) { } } +// TestAppsGitCredentialRemove_IsLocalCleanupWithoutScopes 确认 git credential remove 是本地清理、不带任何 scope。 func TestAppsGitCredentialRemove_IsLocalCleanupWithoutScopes(t *testing.T) { if len(AppsGitCredentialRemove.Scopes) != 0 { t.Fatalf("git credential remove scopes = %#v, want none for local cleanup", AppsGitCredentialRemove.Scopes) } } +// TestAppsGitCredentialList_IsLocalReadWithoutScopes 确认 git credential list 是本地读取、不带任何 scope。 func TestAppsGitCredentialList_IsLocalReadWithoutScopes(t *testing.T) { if len(AppsGitCredentialList.Scopes) != 0 { t.Fatalf("git credential list scopes = %#v, want none for local read", AppsGitCredentialList.Scopes) } } +// TestInstallOnApps_AddsHiddenGitCredentialHelper 验证 InstallOnApps 挂载一个隐藏、带 RunE 且独立于 shortcut 管线的 git-credential-helper 命令。 func TestInstallOnApps_AddsHiddenGitCredentialHelper(t *testing.T) { parent := &cobra.Command{Use: "apps"} InstallOnApps(parent, nil) diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 349535fad..969538e7c 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -407,6 +407,8 @@ func (ctx *RuntimeContext) StreamPages(method, url string, params map[string]int return ac.StreamPages(ctx.ctx, req, onItems, opts) } +// buildRequest assembles a client.RawApiRequest for the current identity, +// attaching any shortcut-scoped header options carried on the context. func (ctx *RuntimeContext) buildRequest(method, url string, params map[string]interface{}, data interface{}) client.RawApiRequest { req := client.RawApiRequest{ Method: method, @@ -421,6 +423,8 @@ func (ctx *RuntimeContext) buildRequest(method, url string, params map[string]in return req } +// callRaw issues the request via the cached APIClient.CallAPI and returns the +// raw result; it is the shared transport behind CallAPI and RawAPI. func (ctx *RuntimeContext) callRaw(method, url string, params map[string]interface{}, data interface{}) (interface{}, error) { ac, err := ctx.getAPIClient() if err != nil { @@ -514,6 +518,8 @@ func (ctx *RuntimeContext) DoAPIJSONTyped(method, apiPath string, query larkcore return ctx.ClassifyAPIResponse(resp) } +// doAPIJSON issues an API request and decodes the response into a JSON map, mapping +// HTTP/business errors to typed output errors (optionally attaching the log id when includeLogID). func (ctx *RuntimeContext) doAPIJSON(method, apiPath string, query larkcore.QueryParams, body any, includeLogID bool) (map[string]any, error) { req := &larkcore.ApiReq{ HttpMethod: method, @@ -821,6 +827,8 @@ func (ctx *RuntimeContext) OutFormatRaw(data interface{}, meta *output.Meta, pre ctx.outFormat(data, meta, prettyFn, true) } +// outFormat renders data according to ctx.Format (pretty/json/table/csv/ndjson), running the +// safety scan and pretty renderer for pretty mode; raw selects the unwrapped output sink. func (ctx *RuntimeContext) outFormat(data interface{}, meta *output.Meta, prettyFn func(w io.Writer), raw bool) { outFn := ctx.Out if raw { @@ -931,12 +939,17 @@ func (s Shortcut) Mount(parent *cobra.Command, f *cmdutil.Factory) { s.MountWithContext(context.Background(), parent, f) } +// MountWithContext registers the shortcut on parent using the given context, +// dispatching to mountDeclarative when the shortcut defines an Execute func. func (s Shortcut) MountWithContext(ctx context.Context, parent *cobra.Command, f *cmdutil.Factory) { if s.Execute != nil { s.mountDeclarative(ctx, parent, f) } } +// mountDeclarative builds the cobra.Command for a declarative shortcut — wiring +// RunE to runShortcut, registering flags, identities, tips and risk, and adding +// it to parent — defaulting AuthTypes to user and deriving the bot-only flag. func (s Shortcut) mountDeclarative(ctx context.Context, parent *cobra.Command, f *cmdutil.Factory) { shortcut := s if len(shortcut.AuthTypes) == 0 { @@ -1064,6 +1077,8 @@ func runShortcut(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bo return rctx.outputErr } +// resolveShortcutIdentity determines the effective identity (--as > default-as > +// auto-detect), enforces strict mode, and verifies the shortcut supports it. func resolveShortcutIdentity(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) (core.Identity, error) { // Step 1: determine identity (--as > default-as > auto-detect). asFlag, _ := cmd.Flags().GetString("as") @@ -1080,6 +1095,9 @@ func resolveShortcutIdentity(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut return as, nil } +// checkShortcutScopes runs the pre-flight scope check for the resolved identity, +// returning a typed PermissionError (with a login hint) when scopes are missing; +// it is a no-op when no scopes are required or scope metadata is unavailable. func checkShortcutScopes(f *cmdutil.Factory, ctx context.Context, as core.Identity, config *core.CliConfig, scopes []string) error { if len(scopes) == 0 { return nil @@ -1098,6 +1116,9 @@ func checkShortcutScopes(f *cmdutil.Factory, ctx context.Context, as core.Identi WithHint("run `lark-cli auth login --scope \"%s\"` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", strings.Join(missing, " ")) } +// newRuntimeContext builds a RuntimeContext for a shortcut invocation: it seeds +// the shortcut context, lazily wires the API client and bot-info providers, +// eagerly initializes the Lark SDK, and reads the format and jq flags. func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, config *core.CliConfig, as core.Identity, botOnly bool) (*RuntimeContext, error) { ctx := cmd.Context() ctx = cmdutil.ContextWithShortcut(ctx, s.Service+":"+s.Command, uuid.New().String()) @@ -1200,6 +1221,8 @@ func resolveInputFlags(rctx *RuntimeContext, flags []Flag) error { return nil } +// validateEnumFlags checks that every flag with an Enum constraint holds an +// allowed value, returning a validation error naming the flag otherwise. func validateEnumFlags(rctx *RuntimeContext, flags []Flag) error { for _, fl := range flags { if len(fl.Enum) == 0 { @@ -1224,6 +1247,8 @@ func validateEnumFlags(rctx *RuntimeContext, flags []Flag) error { return nil } +// handleShortcutDryRun runs the shortcut's DryRun hook and prints its result +// (pretty or JSON per --format), erroring if the shortcut declares no DryRun. func handleShortcutDryRun(f *cmdutil.Factory, rctx *RuntimeContext, s *Shortcut) error { if s.DryRun == nil { return ValidationErrorf("--dry-run is not supported for %s %s", s.Service, s.Command). @@ -1252,10 +1277,16 @@ func rejectPositionalArgs() cobra.PositionalArgs { } } +// registerShortcutFlags registers the shortcut's flags using a background +// context; a convenience wrapper over registerShortcutFlagsWithContext. func registerShortcutFlags(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) { registerShortcutFlagsWithContext(context.Background(), cmd, f, s) } +// registerShortcutFlagsWithContext declares all of the shortcut's flags on cmd — +// typed flags with enum/input hints, completions, required and hidden markers — +// plus the framework flags (--dry-run, --format/--json, --jq, identity, and the +// --print-schema/--flag-name introspection flags when opted in). func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) { for _, fl := range s.Flags { desc := fl.Desc