mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
Drive-domain errors now leave the CLI as typed, machine-branchable envelopes — a stable `type` plus `subtype` and named fields (param, params, retryable, log_id, hint) — so scripts and AI agents can branch on structure and act on a recovery hint instead of parsing prose. Changes: - Every error produced in the drive domain — validation, file I/O, and the failures returned from its Lark API calls — is emitted as a typed errs.* error; the exit code is derived from the error category. Drive's API calls now go through a shared typed classifier, so failures carry subtype, troubleshooter, a recovery hint, and the request's log_id whether the server returns it in the response body or the x-tt-logid header; an already-typed network/auth error is never downgraded into a generic API error. - Known API conditions (resource conflict, cross-tenant, cross-brand, ...) carry a recovery hint keyed by their error class; a command can refine that hint with command-specific guidance. - Batch partial failures (+push / +pull / +sync, where some items succeed and some fail) now report an honest ok:false multi-status result on stdout — the summary and every per-item outcome stay machine-readable — and exit non-zero, instead of a misleading ok:true success envelope. - Duplicate rel_path conflicts report each colliding path as a structured params entry (RFC 7807 invalid-params style). - Static guards lock the drive path so legacy error construction — direct envelopes or the auto-classifying API helpers — cannot be reintroduced, making drive the template for the remaining domains. Output changes worth noting for consumers: - Error envelopes now carry typed type/subtype and named fields; exit codes follow the error category (malformed or incomplete API responses are reported as internal errors rather than generic API errors). - Batch partial failures (+push / +pull / +sync) emit an ok:false result envelope on stdout (summary + per-item items[]) and exit non-zero; the per-item results stay on stdout rather than in a stderr error envelope. Errors surfaced through shared cross-domain helpers (scope precheck, media import upload, metadata lookup, save-path resolution) are not yet typed; they migrate with the shared layer in a follow-up change.
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package common
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
)
|
|
|
|
// TestOutPartialFailure pins the batch / multi-status contract: the result
|
|
// rides on stdout as an ok:false envelope (carrying the full payload), and the
|
|
// returned error is the typed partial-failure exit signal (ExitAPI), distinct
|
|
// from the predicate-only ErrBare.
|
|
func TestOutPartialFailure(t *testing.T) {
|
|
cfg := &core.CliConfig{Brand: core.BrandFeishu, AppID: "cli_x"}
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, cfg)
|
|
rt := TestNewRuntimeContextForAPI(context.Background(), &cobra.Command{Use: "+push"}, cfg, f, core.AsUser)
|
|
|
|
payload := map[string]interface{}{
|
|
"summary": map[string]interface{}{"uploaded": 1, "failed": 1},
|
|
"items": []map[string]interface{}{
|
|
{"rel_path": "a.txt", "action": "uploaded"},
|
|
{"rel_path": "b.txt", "action": "failed", "error": "boom"},
|
|
},
|
|
}
|
|
|
|
err := rt.OutPartialFailure(payload, nil)
|
|
|
|
// 1) typed partial-failure exit signal
|
|
var pfErr *output.PartialFailureError
|
|
if !errors.As(err, &pfErr) {
|
|
t.Fatalf("expected *output.PartialFailureError, got %T: %v", err, err)
|
|
}
|
|
if pfErr.Code != output.ExitAPI {
|
|
t.Errorf("exit code = %d, want %d (ExitAPI)", pfErr.Code, output.ExitAPI)
|
|
}
|
|
|
|
// 2) stdout envelope reports ok:false but still carries the full payload
|
|
// (both the succeeded and failed items) — consistent with a success Out().
|
|
var env struct {
|
|
OK bool `json:"ok"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
|
t.Fatalf("unmarshal stdout envelope: %v\nstdout: %s", err, stdout.String())
|
|
}
|
|
if env.OK {
|
|
t.Errorf("ok must be false on partial failure, got ok:true\nstdout: %s", stdout.String())
|
|
}
|
|
items, _ := env.Data["items"].([]interface{})
|
|
if len(items) != 2 {
|
|
t.Fatalf("both succeeded and failed items must ride on stdout, got %d items\nstdout: %s", len(items), stdout.String())
|
|
}
|
|
}
|