From fb33a7fc585cb731e218f86817ec19ad0153ce3e Mon Sep 17 00:00:00 2001 From: buty4649 Date: Fri, 17 Apr 2026 17:27:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?document:=20status=20=E3=82=92=20TTY=20?= =?UTF-8?q?=E5=90=91=E3=81=91=E3=81=AB=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB?= =?UTF-8?q?/=E3=83=AA=E3=82=B9=E3=83=88=E8=A1=A8=E7=A4=BA=E3=81=B8?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tty環境でも生 JSON が出力されて読みづらかったため、 他サブコマンドと同じ --output / --jq 規約に合わせ、 TTY では書類メタ情報 (DOCID/TITLE/FORM/ROUTE/STATUS/STEP/ WRITER/LASTAPRV) を newList で、承認フローを newTable で 整形表示するよう変更。--output json または非 TTY 時は従来 通りサーバ応答 JSON をそのまま出力する。 承認フロー表示は次の工夫を含む: - 現在ステップは "*2"、その他は " 2" と空白パディングで 列幅を揃え、カレント位置を視認しやすくする - STEP/TITLE が直前行と同じ場合は両セルを空欄にして 承認者行をグルーピング表示する - 最終行に "承認完了" 行 (STEP = 最終+1) を追加。status.code が 6 のときは LASTAPRV 情報を表示し "*" を付与する - サーバが整数フィールドを文字列で返すケース (status.code など) に対応する flexInt 型でデコードする Closes #30 Co-Authored-By: Claude Opus 4.7 --- cmd/document.go | 394 +++++++++++++++++++++++++++++++++++++++++- cmd/document_test.go | 396 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+), 7 deletions(-) diff --git a/cmd/document.go b/cmd/document.go index 93790a3..fc57eb9 100644 --- a/cmd/document.go +++ b/cmd/document.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "fmt" @@ -50,6 +51,7 @@ var ( docDownloadOutput string docStatusHistory bool + docStatusOutput string docStatusJQ string docOpenNoBrowser bool @@ -187,9 +189,10 @@ var documentStatusCmd = &cobra.Command{ Short: "Get document approval status", Long: `Retrieve the approval status of a document via GET /api/v1/documents/{docid}/status. -The response is returned as JSON and contains the current status, step, -writer, last approver, and the approval flow. Pass --history to include -approval histories for all past versions.`, +The response contains the current status, step, writer, last approver, and +the approval flow. On a TTY, a human-readable summary is printed by default; +otherwise raw JSON is emitted. Pass --output json/table to force a format. +Pass --history to include approval histories for all past versions.`, Args: cobra.ExactArgs(1), RunE: runDocumentStatus, } @@ -375,7 +378,8 @@ func init() { sf := documentStatusCmd.Flags() sf.BoolVar(&docStatusHistory, "history", false, "include approval histories for all versions") - sf.StringVar(&docStatusJQ, "jq", "", "apply a gojq filter to the JSON response") + sf.StringVarP(&docStatusOutput, "output", "o", "", "output format: table|json (default: table on TTY, json otherwise)") + sf.StringVar(&docStatusJQ, "jq", "", "apply a gojq filter to the JSON response (forces JSON output)") dlf := documentDownloadCmd.Flags() dlf.StringVarP(&docDownloadOutput, "output", "o", "", "output path: FILE, DIR/, or - for stdout (default: server-provided filename in current directory)") @@ -630,10 +634,386 @@ func runDocumentStatus(cmd *cobra.Command, args []string) error { if err != nil { return err } - if docStatusJQ != "" { - return runJQ(raw, docStatusJQ) + + format := resolveOutputFormat(docStatusOutput) + if docStatusJQ == "" && format == "json" { + // Preserve the exact server JSON (without re-marshaling the struct), + // matching the prior behavior when stdout is not a TTY. + _, err := os.Stdout.Write(formatRawJSONIndent(raw)) + return err } - return writeJSON(os.Stdout, raw) + + view, err := parseDocumentStatus(raw) + if err != nil { + return err + } + + return render(view, format, docStatusJQ, func() error { + printDocumentStatusTable(os.Stdout, view) + return nil + }) +} + +type documentStatusView struct { + Document documentStatusDocument `json:"document"` +} + +type documentStatusDocument struct { + DocID flexInt `json:"docid"` + Title1 string `json:"title1"` + Title2 string `json:"title2"` + Form documentStatusForm `json:"form"` + Route documentStatusRoute `json:"route"` + Type string `json:"type"` + Status documentStatusState `json:"status"` + Step documentStatusStep `json:"step"` + CurrentVersion flexInt `json:"current_version"` + Writer documentStatusUser `json:"writer"` + LastAprv documentStatusUser `json:"lastaprv"` + FlowVersions []documentStatusFlowVer `json:"flow_versions"` + Histories []documentStatusHistory `json:"histories"` +} + +type documentStatusForm struct { + ID flexInt `json:"id"` + Code string `json:"code"` + Name string `json:"name"` +} + +type documentStatusRoute struct { + Code string `json:"code"` + Name string `json:"name"` +} + +type documentStatusState struct { + Code flexInt `json:"code"` + Name string `json:"name"` +} + +type documentStatusStep struct { + Max flexInt `json:"max"` + Current flexInt `json:"current"` +} + +// flexInt decodes JSON numbers that may arrive as either an integer or a +// string (some X-point endpoints return integer-valued fields as strings). +type flexInt int + +func (f *flexInt) UnmarshalJSON(b []byte) error { + if len(b) == 0 || string(b) == "null" { + *f = 0 + return nil + } + // Numeric form. + if b[0] != '"' { + var n int + if err := json.Unmarshal(b, &n); err != nil { + return err + } + *f = flexInt(n) + return nil + } + // String form — tolerate empty/whitespace. + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + s = strings.TrimSpace(s) + if s == "" { + *f = 0 + return nil + } + n, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("flexInt: %w", err) + } + *f = flexInt(n) + return nil +} + +type documentStatusUser struct { + UserCode string `json:"usercode"` + UserName string `json:"username"` + StampName string `json:"stampname"` + DateTime string `json:"datetime"` +} + +type documentStatusFlowVer struct { + FlowResults []documentStatusFlowStep `json:"flow_results"` +} + +type documentStatusHistory struct { + Version flexInt `json:"version"` + FlowResults []documentStatusFlowStep `json:"flow_results"` +} + +type documentStatusFlowStep struct { + StepNo flexInt `json:"stepno"` + StepTitle string `json:"steptitle"` + AprvUsers []documentStatusAprvUser `json:"aprvusers"` + Cond string `json:"cond"` + CondNum flexInt `json:"cond_num"` + AdminSkip flexInt `json:"adminskip"` + Skip flexInt `json:"skip"` + BackStepNo flexInt `json:"backstepno"` +} + +type documentStatusAprvUser struct { + Aprv documentStatusAprvDetail `json:"aprv"` + StatusCode flexInt `json:"statuscode"` + Status string `json:"status"` +} + +type documentStatusAprvDetail struct { + UserCode string `json:"usercode"` + UserName string `json:"username"` + StampName string `json:"stampname"` + DateTime string `json:"datetime"` + GroupCD string `json:"groupcd"` + GroupName string `json:"groupname"` + PartCD string `json:"partcd"` + PartName string `json:"partname"` +} + +func parseDocumentStatus(raw json.RawMessage) (*documentStatusView, error) { + var view documentStatusView + if err := json.Unmarshal(raw, &view); err != nil { + return nil, fmt.Errorf("parse status response: %w", err) + } + return &view, nil +} + +func formatRawJSONIndent(raw json.RawMessage) []byte { + var buf bytes.Buffer + if err := json.Indent(&buf, raw, "", " "); err != nil { + // Fall back to the raw bytes if they cannot be re-indented. + buf.Reset() + buf.Write(raw) + } + buf.WriteByte('\n') + return buf.Bytes() +} + +func printDocumentStatusTable(out io.Writer, view *documentStatusView) { + d := view.Document + list := newList(out) + list.AddRow("DOCID:", fmt.Sprint(d.DocID)) + list.AddRow("TITLE1:", dashIfEmpty(d.Title1)) + if d.Title2 != "" { + list.AddRow("TITLE2:", d.Title2) + } + list.AddRow("FORM:", formatFormRef(d.Form)) + if d.Route.Code != "" || d.Route.Name != "" { + list.AddRow("ROUTE:", formatRouteRef(d.Route)) + } + list.AddRow("STATUS:", formatStatusState(d.Status)) + list.AddRow("STEP:", formatStepInfo(d.Step)) + list.AddRow("WRITER:", formatUserStamp(d.Writer)) + if d.LastAprv.UserCode != "" || d.LastAprv.UserName != "" || d.LastAprv.DateTime != "" { + list.AddRow("LASTAPRV:", formatUserStamp(d.LastAprv)) + } + list.Print() + + for i, fv := range d.FlowVersions { + if len(fv.FlowResults) == 0 { + continue + } + fmt.Fprintln(out) + if len(d.FlowVersions) == 1 { + fmt.Fprintln(out, "承認フロー:") + } else { + fmt.Fprintf(out, "承認フロー (flow_versions[%d]):\n", i) + } + printFlowResultsTable(out, fv.FlowResults, flowCurrentStepNo(d), buildCompletionRow(d)) + } + + for _, h := range d.Histories { + fmt.Fprintln(out) + fmt.Fprintf(out, "承認履歴 (version %d):\n", h.Version) + if len(h.FlowResults) == 0 { + fmt.Fprintln(out, " (no steps)") + continue + } + printFlowResultsTable(out, h.FlowResults, 0, nil) + } +} + +type flowCompletionRow struct { + user, status, datetime string + current bool +} + +// buildCompletionRow returns the trailing "承認完了" row for the current +// version's approval flow. When the document is actually completed (status +// code 6), the last approver's name/datetime are filled in and current is +// true so the row carries the "*" marker; otherwise each cell is a dash so +// the row still anchors the end of the flow. +func buildCompletionRow(d documentStatusDocument) *flowCompletionRow { + if int(d.Status.Code) == 6 { + name := d.LastAprv.UserName + if name == "" { + name = d.LastAprv.StampName + } + return &flowCompletionRow{ + user: dashIfEmpty(name), + status: dashIfEmpty(d.Status.Name), + datetime: dashIfEmpty(d.LastAprv.DateTime), + current: true, + } + } + return &flowCompletionRow{user: "-", status: "-", datetime: "-"} +} + +// flowCurrentStepNo picks the step number that carries the "*" marker. +// For a completed document the marker belongs on the trailing 承認完了 row, +// so we return 0 here to suppress it on every real step. +func flowCurrentStepNo(d documentStatusDocument) int { + if int(d.Status.Code) == 6 { + return 0 + } + return int(d.Step.Current) +} + +// printFlowResultsTable prints the approval flow as a table. When +// currentStepNo > 0, the matching step number is marked with "*" so the +// reader can spot the pending step at a glance. Consecutive rows sharing +// the same STEP/TITLE collapse those two cells so each step reads as one +// group with multiple approvers underneath. When completion is non-nil a +// trailing "承認完了" row is appended. +func printFlowResultsTable(out io.Writer, steps []documentStatusFlowStep, currentStepNo int, completion *flowCompletionRow) { + w := newTable(out, "STEP", "TITLE", "USER", "STATUS", "DATETIME") + prevStep, prevTitle := "", "" + emit := func(step, title, user, status, datetime string) { + stepOut, titleOut := step, title + if step == prevStep && title == prevTitle { + stepOut, titleOut = "", "" + } else { + prevStep, prevTitle = step, title + } + w.AddRow(stepOut, titleOut, user, status, datetime) + } + for _, s := range steps { + step := formatFlowStepNo(int(s.StepNo), currentStepNo) + title := dashIfEmpty(s.StepTitle) + if len(s.AprvUsers) == 0 { + emit(step, title, "-", "-", "-") + continue + } + for _, u := range s.AprvUsers { + emit(step, title, + formatAprvUser(u.Aprv), + dashIfEmpty(u.Status), + dashIfEmpty(u.Aprv.DateTime), + ) + } + } + if completion != nil { + completionStepNo := 0 + if len(steps) > 0 { + completionStepNo = int(steps[len(steps)-1].StepNo) + 1 + } + var stepStr string + if completionStepNo == 0 { + stepStr = "" + } else if completion.current { + stepStr = "*" + strconv.Itoa(completionStepNo) + } else { + stepStr = " " + strconv.Itoa(completionStepNo) + } + emit(stepStr, "承認完了", completion.user, completion.status, completion.datetime) + } + w.Print() +} + +// formatFlowStepNo renders the STEP column. The current step is prefixed +// with "*"; other steps get a leading space so numbers stay column-aligned +// (e.g. "*1" next to " 2"). +func formatFlowStepNo(stepno, currentStepNo int) string { + if currentStepNo > 0 && stepno == currentStepNo { + return "*" + strconv.Itoa(stepno) + } + return " " + strconv.Itoa(stepno) +} + +func formatFormRef(f documentStatusForm) string { + switch { + case f.Name != "": + return f.Name + case f.Code != "": + return f.Code + default: + return "-" + } +} + +func formatRouteRef(r documentStatusRoute) string { + switch { + case r.Code != "" && r.Name != "": + return fmt.Sprintf("%s %s", r.Code, r.Name) + case r.Name != "": + return r.Name + case r.Code != "": + return r.Code + default: + return "-" + } +} + +func formatStatusState(s documentStatusState) string { + if s.Name != "" { + return s.Name + } + if s.Code != 0 { + return strconv.Itoa(int(s.Code)) + } + return "-" +} + +func formatStepInfo(s documentStatusStep) string { + if s.Max == 0 && s.Current == 0 { + return "-" + } + return fmt.Sprintf("%d/%d", s.Current, s.Max) +} + +func formatUserStamp(u documentStatusUser) string { + name := u.UserName + if name == "" { + name = u.StampName + } + if name == "" { + name = u.UserCode + } + switch { + case name != "" && u.DateTime != "": + return fmt.Sprintf("%s %s", name, u.DateTime) + case name != "": + return name + case u.DateTime != "": + return u.DateTime + default: + return "-" + } +} + +func formatAprvUser(a documentStatusAprvDetail) string { + switch { + case a.UserName != "": + return a.UserName + case a.StampName != "": + return a.StampName + case a.UserCode != "": + return a.UserCode + default: + return "-" + } +} + +func dashIfEmpty(s string) string { + if s == "" { + return "-" + } + return s } func runDocumentDownload(cmd *cobra.Command, args []string) error { diff --git a/cmd/document_test.go b/cmd/document_test.go index b9b6882..f1d5882 100644 --- a/cmd/document_test.go +++ b/cmd/document_test.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "io" @@ -386,6 +387,401 @@ func TestRunDocumentEdit_InvalidDocID(t *testing.T) { } } +func TestParseDocumentStatus_PopulatesFields(t *testing.T) { + raw := json.RawMessage(`{ + "document": { + "docid": 12345, + "title1": "経費申請 2024-01", + "form": {"id": 99, "code": "form01", "name": "経費申請書"}, + "route": {"code": "r1", "name": "標準ルート"}, + "type": "workflow", + "status": {"code": 1, "name": "承認中"}, + "step": {"max": 5, "current": 2}, + "current_version": 3, + "writer": {"usercode": "u001", "username": "山田太郎", "datetime": "2024-01-15 10:00:00"}, + "lastaprv": {"usercode": "u002", "username": "佐藤花子", "datetime": "2024-01-16 11:00:00"}, + "flow_versions": [ + { + "flow_results": [ + {"stepno": 1, "steptitle": "申請", "aprvusers": [ + {"aprv": {"usercode": "u001", "username": "山田太郎", "datetime": "2024-01-15 10:00:00"}, "statuscode": 0, "status": "申請"} + ]}, + {"stepno": 2, "steptitle": "一次承認", "aprvusers": [ + {"aprv": {"usercode": "u002", "username": "佐藤花子", "datetime": "2024-01-16 11:00:00"}, "statuscode": 1, "status": "承認"} + ]} + ] + } + ] + } + }`) + + view, err := parseDocumentStatus(raw) + if err != nil { + t.Fatalf("parseDocumentStatus: %v", err) + } + d := view.Document + if d.DocID != 12345 { + t.Errorf("docid = %d", d.DocID) + } + if d.Form.Code != "form01" || d.Form.Name != "経費申請書" { + t.Errorf("form = %+v", d.Form) + } + if d.Status.Code != 1 || d.Status.Name != "承認中" { + t.Errorf("status = %+v", d.Status) + } + if d.Step.Current != 2 || d.Step.Max != 5 { + t.Errorf("step = %+v", d.Step) + } + if len(d.FlowVersions) != 1 || len(d.FlowVersions[0].FlowResults) != 2 { + t.Fatalf("flow_versions = %+v", d.FlowVersions) + } + first := d.FlowVersions[0].FlowResults[1] + if first.StepTitle != "一次承認" || len(first.AprvUsers) != 1 { + t.Errorf("flow[1] = %+v", first) + } + if first.AprvUsers[0].Aprv.UserName != "佐藤花子" { + t.Errorf("approver = %+v", first.AprvUsers[0]) + } +} + +func TestPrintDocumentStatusTable_IncludesMetaAndFlow(t *testing.T) { + view := &documentStatusView{ + Document: documentStatusDocument{ + DocID: 12345, + Title1: "経費申請", + Form: documentStatusForm{ID: 99, Code: "form01", Name: "経費申請書"}, + Route: documentStatusRoute{Code: "r1", Name: "標準ルート"}, + Type: "workflow", + Status: documentStatusState{Code: 1, Name: "承認中"}, + Step: documentStatusStep{Max: 5, Current: 2}, + CurrentVersion: 3, + Writer: documentStatusUser{ + UserCode: "u001", UserName: "山田太郎", + DateTime: "2024-01-15 10:00:00", + }, + LastAprv: documentStatusUser{ + UserCode: "u002", UserName: "佐藤花子", + DateTime: "2024-01-16 11:00:00", + }, + FlowVersions: []documentStatusFlowVer{ + {FlowResults: []documentStatusFlowStep{ + {StepNo: 1, StepTitle: "申請", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u001", UserName: "山田太郎", DateTime: "2024-01-15 10:00:00"}, Status: "申請"}, + }}, + {StepNo: 2, StepTitle: "一次承認", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u002", UserName: "佐藤花子"}, Status: "未処理"}, + {Aprv: documentStatusAprvDetail{UserCode: "u003", UserName: "鈴木次郎"}, Status: "未処理"}, + }}, + }}, + }, + }, + } + + var buf bytes.Buffer + printDocumentStatusTable(&buf, view) + out := buf.String() + + for _, want := range []string{ + "DOCID:", "12345", + "TITLE1:", "経費申請", + "FORM:", "経費申請書", + "ROUTE:", "標準ルート", + "STATUS:", "承認中", + "STEP:", "2/5", + "WRITER:", "山田太郎", "2024-01-15 10:00:00", + "LASTAPRV:", "佐藤花子", + "承認フロー:", + "STEP", "TITLE", "USER", "STATUS", "DATETIME", + "申請", "一次承認", + "山田太郎", "佐藤花子", "鈴木次郎", + } { + if !strings.Contains(out, want) { + t.Errorf("missing %q in output:\n%s", want, out) + } + } + // TYPE / VERSION should no longer be rendered. + for _, unwant := range []string{"TYPE:", "VERSION:"} { + if strings.Contains(out, unwant) { + t.Errorf("unwanted label %q present in output:\n%s", unwant, out) + } + } + // FORM should not display the form code (only the name). + if strings.Contains(out, "form01") { + t.Errorf("FORM line should not include the form code, got:\n%s", out) + } + // STATUS should not include the numeric status code. + if strings.Contains(out, "(1)") { + t.Errorf("STATUS line should not include numeric code, got:\n%s", out) + } + // Neither the flow table USER column nor WRITER/LASTAPRV should include + // the parenthesized user code. + for _, unwant := range []string{"(u001)", "(u002)", "(u003)"} { + if strings.Contains(out, unwant) { + t.Errorf("user code %q should not appear anywhere in output:\n%s", unwant, out) + } + } + // Current step (2) should be marked with "*2"; non-current step (1) + // gets a leading space " 1" to keep numbers column-aligned. + if !strings.Contains(out, "*2") { + t.Errorf("current step marker \"*2\" missing:\n%s", out) + } + if !strings.Contains(out, " 1") { + t.Errorf("non-current step \" 1\" missing:\n%s", out) + } + // Trailing 承認完了 row should be appended. + if !strings.Contains(out, "承認完了") { + t.Errorf("trailing 承認完了 row missing:\n%s", out) + } + // The second approver row should have blank STEP/TITLE because it + // shares the step with the first approver. + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + var suzukiLine string + for _, l := range lines { + if strings.Contains(l, "鈴木次郎") { + suzukiLine = l + break + } + } + if suzukiLine == "" { + t.Fatalf("鈴木次郎 row missing in output:\n%s", out) + } + if strings.Contains(suzukiLine, "一次承認") || strings.Contains(suzukiLine, "*2") { + t.Errorf("grouped row should have blank STEP/TITLE, got: %q", suzukiLine) + } +} + +func TestBuildCompletionRow_CompletedShowsLastAprv(t *testing.T) { + d := documentStatusDocument{ + Status: documentStatusState{Code: 6, Name: "承認完了"}, + LastAprv: documentStatusUser{ + UserCode: "u9", UserName: "承認太郎", + DateTime: "2024-02-01 09:00:00", + }, + } + r := buildCompletionRow(d) + if r == nil { + t.Fatal("completion row = nil, want populated row") + } + if r.user != "承認太郎" { + t.Errorf("user = %q", r.user) + } + if r.status != "承認完了" { + t.Errorf("status = %q", r.status) + } + if r.datetime != "2024-02-01 09:00:00" { + t.Errorf("datetime = %q", r.datetime) + } + if !r.current { + t.Errorf("current = false, want true for 承認完了") + } +} + +func TestBuildCompletionRow_NotCompletedShowsDashes(t *testing.T) { + d := documentStatusDocument{ + Status: documentStatusState{Code: 1, Name: "承認中"}, + LastAprv: documentStatusUser{}, + } + r := buildCompletionRow(d) + if r == nil { + t.Fatal("completion row = nil") + } + if r.user != "-" || r.status != "-" || r.datetime != "-" { + t.Errorf("row = %+v, want all dashes", r) + } + if r.current { + t.Errorf("current = true, want false for non-completed") + } +} + +func TestFlowCurrentStepNo_SuppressedWhenCompleted(t *testing.T) { + d := documentStatusDocument{ + Status: documentStatusState{Code: 6, Name: "承認完了"}, + Step: documentStatusStep{Max: 3, Current: 3}, + } + if got := flowCurrentStepNo(d); got != 0 { + t.Errorf("completed doc: got %d, want 0", got) + } +} + +func TestFlowCurrentStepNo_UsesStepCurrent(t *testing.T) { + d := documentStatusDocument{ + Status: documentStatusState{Code: 1, Name: "承認中"}, + Step: documentStatusStep{Max: 3, Current: 2}, + } + if got := flowCurrentStepNo(d); got != 2 { + t.Errorf("pending doc: got %d, want 2", got) + } +} + +func TestPrintDocumentStatusTable_CompletedMarksFinalRow(t *testing.T) { + view := &documentStatusView{ + Document: documentStatusDocument{ + DocID: 42, + Form: documentStatusForm{Code: "f", Name: "F"}, + Status: documentStatusState{Code: 6, Name: "承認完了"}, + Step: documentStatusStep{Max: 3, Current: 3}, + LastAprv: documentStatusUser{ + UserCode: "u9", UserName: "承認太郎", + DateTime: "2024-02-01 09:00:00", + }, + FlowVersions: []documentStatusFlowVer{ + {FlowResults: []documentStatusFlowStep{ + {StepNo: 1, StepTitle: "申請", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u1", UserName: "A"}, Status: "申請"}, + }}, + {StepNo: 3, StepTitle: "最終承認", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u9", UserName: "承認太郎"}, Status: "承認"}, + }}, + }}, + }, + }, + } + + var buf bytes.Buffer + printDocumentStatusTable(&buf, view) + out := buf.String() + + if !strings.Contains(out, "*4") { + t.Errorf("承認完了 row should carry \"*4\" marker (last+1), got:\n%s", out) + } + // The real last step (3) should NOT be starred when the doc is completed. + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + var lastStepLine string + for _, l := range lines { + if strings.Contains(l, "最終承認") { + lastStepLine = l + break + } + } + if lastStepLine == "" { + t.Fatalf("最終承認 row missing:\n%s", out) + } + if strings.Contains(lastStepLine, "*3") { + t.Errorf("real step row should not be starred when completed, got: %q", lastStepLine) + } + if !strings.Contains(lastStepLine, " 3") { + t.Errorf("real step row should have \" 3\" prefix, got: %q", lastStepLine) + } +} + +func TestFormatFlowStepNo_StarAndSpacePrefix(t *testing.T) { + if got := formatFlowStepNo(2, 2); got != "*2" { + t.Errorf("current step: got %q, want \"*2\"", got) + } + if got := formatFlowStepNo(1, 2); got != " 1" { + t.Errorf("non-current step: got %q, want \" 1\"", got) + } + if got := formatFlowStepNo(3, 0); got != " 3" { + t.Errorf("currentStepNo=0: got %q, want \" 3\"", got) + } +} + +func TestPrintDocumentStatusTable_IncludesHistories(t *testing.T) { + view := &documentStatusView{ + Document: documentStatusDocument{ + DocID: 42, + Form: documentStatusForm{Code: "f", Name: "F"}, + Status: documentStatusState{Code: 6, Name: "承認完了"}, + Step: documentStatusStep{Max: 2, Current: 2}, + Histories: []documentStatusHistory{ + {Version: 1, FlowResults: []documentStatusFlowStep{ + {StepNo: 1, StepTitle: "申請", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u1", UserName: "A"}, Status: "申請"}, + }}, + }}, + {Version: 2, FlowResults: []documentStatusFlowStep{ + {StepNo: 1, StepTitle: "申請", AprvUsers: []documentStatusAprvUser{ + {Aprv: documentStatusAprvDetail{UserCode: "u1", UserName: "A"}, Status: "申請"}, + }}, + }}, + }, + }, + } + + var buf bytes.Buffer + printDocumentStatusTable(&buf, view) + out := buf.String() + + for _, want := range []string{ + "承認履歴 (version 1):", + "承認履歴 (version 2):", + } { + if !strings.Contains(out, want) { + t.Errorf("missing %q in output:\n%s", want, out) + } + } +} + +func TestParseDocumentStatus_AcceptsStringNumbers(t *testing.T) { + // Some X-point endpoints return integer-valued fields as JSON strings. + raw := json.RawMessage(`{ + "document": { + "docid": "265941", + "form": {"id": "99", "code": "f", "name": "F"}, + "status": {"code": "1", "name": "承認中"}, + "step": {"max": "3", "current": "2"}, + "current_version": "4", + "flow_versions": [ + {"flow_results": [ + {"stepno": "1", "steptitle": "申請", "aprvusers": [ + {"aprv": {"usercode": "u1", "username": "A"}, "statuscode": "0", "status": "申請"} + ]} + ]} + ], + "histories": [ + {"version": "1", "flow_results": []} + ] + } + }`) + + view, err := parseDocumentStatus(raw) + if err != nil { + t.Fatalf("parseDocumentStatus: %v", err) + } + d := view.Document + if d.DocID != 265941 { + t.Errorf("docid = %d", d.DocID) + } + if d.Status.Code != 1 { + t.Errorf("status.code = %d", d.Status.Code) + } + if d.Step.Current != 2 || d.Step.Max != 3 { + t.Errorf("step = %+v", d.Step) + } + if d.CurrentVersion != 4 { + t.Errorf("current_version = %d", d.CurrentVersion) + } + if len(d.FlowVersions) != 1 || d.FlowVersions[0].FlowResults[0].StepNo != 1 { + t.Errorf("flow_versions = %+v", d.FlowVersions) + } + if d.Histories[0].Version != 1 { + t.Errorf("histories[0].version = %d", d.Histories[0].Version) + } +} + +func TestFlexInt_EmptyStringBecomesZero(t *testing.T) { + var v struct { + X flexInt `json:"x"` + } + if err := json.Unmarshal([]byte(`{"x":""}`), &v); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if v.X != 0 { + t.Errorf("x = %d, want 0", v.X) + } +} + +func TestFormatRawJSONIndent_PrettyPrints(t *testing.T) { + raw := json.RawMessage(`{"a":1,"b":[1,2]}`) + got := string(formatRawJSONIndent(raw)) + if !strings.Contains(got, "\n \"a\": 1") { + t.Errorf("not indented: %q", got) + } + if !strings.HasSuffix(got, "\n") { + t.Errorf("missing trailing newline: %q", got) + } +} + func TestLoadSearchBody_Stdin(t *testing.T) { orig := os.Stdin t.Cleanup(func() { os.Stdin = orig }) From f705fdc319c84977ecc629508d3ee0811260b6a8 Mon Sep 17 00:00:00 2001 From: buty4649 Date: Fri, 17 Apr 2026 17:47:00 +0900 Subject: [PATCH 2/2] =?UTF-8?q?lint:=20if-else=20=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E3=82=92=20switch=20=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E6=8F=9B=20(gocritic=20ifElseChain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- cmd/document.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/document.go b/cmd/document.go index fc57eb9..0c6b77d 100644 --- a/cmd/document.go +++ b/cmd/document.go @@ -913,11 +913,12 @@ func printFlowResultsTable(out io.Writer, steps []documentStatusFlowStep, curren completionStepNo = int(steps[len(steps)-1].StepNo) + 1 } var stepStr string - if completionStepNo == 0 { + switch { + case completionStepNo == 0: stepStr = "" - } else if completion.current { + case completion.current: stepStr = "*" + strconv.Itoa(completionStepNo) - } else { + default: stepStr = " " + strconv.Itoa(completionStepNo) } emit(stepStr, "承認完了", completion.user, completion.status, completion.datetime)