`dws report entry submit --contents-file <f>` (and `--contents -` stdin) silently submitted `contents:[null]`. Root cause: the `--contents-file` flag had no transform, so its value mapped to an unused param while `--contents` stayed empty. - file_read_json transform + a build-time report hook resolve --contents-file / --contents - / @file natively in Go (priority: file > stdin > inline) and declare a contents / contents-file one-of group so a file-only invocation is no longer rejected at parse time. - generalizes to @file / @- input for any structured JSON-array flag.
199 lines
6.9 KiB
Go
199 lines
6.9 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package compat
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// newReportSubmitStub mirrors the leaf command shape emitted by
|
|
// BuildDynamicCommands for `report entry submit` (envelope: create_report).
|
|
// Only the flags the hook touches are registered. --contents is marked
|
|
// required to reproduce the envelope's MarkFlagRequired so the relaxation
|
|
// behaviour can be asserted.
|
|
func newReportSubmitStub() *cobra.Command {
|
|
cmd := &cobra.Command{Use: "submit", RunE: func(*cobra.Command, []string) error { return nil }}
|
|
cmd.Flags().String("contents", "", "contents JSON array")
|
|
cmd.Flags().String("contents-file", "", "contents JSON file")
|
|
cmd.Flags().String("template-id", "", "template id")
|
|
_ = cmd.MarkFlagRequired("contents")
|
|
return cmd
|
|
}
|
|
|
|
const reportContentsPayload = `[{"key":"今日完成工作","sort":"0","content":"done","contentType":"markdown","type":"1"}]`
|
|
|
|
func TestResolveReportContents_FromFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "contents.json")
|
|
if err := os.WriteFile(path, []byte(reportContentsPayload), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
|
|
cmd := newReportSubmitStub()
|
|
if err := cmd.Flags().Set("contents-file", path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := resolveReportContents(cmd); err != nil {
|
|
t.Fatalf("resolveReportContents(file): %v", err)
|
|
}
|
|
got, _ := cmd.Flags().GetString("contents")
|
|
if got != reportContentsPayload {
|
|
t.Fatalf("--contents not populated from file: %q", got)
|
|
}
|
|
// contents-file must be cleared so omitWhen:empty drops the dead param.
|
|
if cf, _ := cmd.Flags().GetString("contents-file"); cf != "" {
|
|
t.Fatalf("--contents-file should be cleared after resolution, got %q", cf)
|
|
}
|
|
}
|
|
|
|
func TestResolveReportContents_FromStdin(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
if err := cmd.Flags().Set("contents", "-"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd.SetIn(strings.NewReader(reportContentsPayload))
|
|
if err := resolveReportContents(cmd); err != nil {
|
|
t.Fatalf("resolveReportContents(stdin): %v", err)
|
|
}
|
|
got, _ := cmd.Flags().GetString("contents")
|
|
if got != reportContentsPayload {
|
|
t.Fatalf("--contents not populated from stdin: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveReportContents_InlineUntouched(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
if err := cmd.Flags().Set("contents", reportContentsPayload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := resolveReportContents(cmd); err != nil {
|
|
t.Fatalf("resolveReportContents(inline): %v", err)
|
|
}
|
|
got, _ := cmd.Flags().GetString("contents")
|
|
if got != reportContentsPayload {
|
|
t.Fatalf("inline --contents must be left untouched, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveReportContents_FilePriorityOverInline(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "contents.json")
|
|
if err := os.WriteFile(path, []byte(reportContentsPayload), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd := newReportSubmitStub()
|
|
if err := cmd.Flags().Set("contents", `[{"stale":"inline"}]`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.Flags().Set("contents-file", path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := resolveReportContents(cmd); err != nil {
|
|
t.Fatalf("resolveReportContents: %v", err)
|
|
}
|
|
got, _ := cmd.Flags().GetString("contents")
|
|
if got != reportContentsPayload {
|
|
t.Fatalf("--contents-file must win over inline --contents, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveReportContents_MissingFileErrors(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
if err := cmd.Flags().Set("contents-file", filepath.Join(t.TempDir(), "nope.json")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := resolveReportContents(cmd)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing --contents-file path")
|
|
}
|
|
if !strings.Contains(err.Error(), "file not found") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// ── installReportHook composition ──────────────────────────────
|
|
|
|
func TestInstallReportHook_RelaxesRequiredToOneOf(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
// Before the hook, --contents carries the cobra required annotation.
|
|
if cmd.Flags().Lookup("contents").Annotations[cobra.BashCompOneRequiredFlag] == nil {
|
|
t.Fatal("precondition: --contents should start out required")
|
|
}
|
|
installReportHook(cmd, "report", "create_report")
|
|
// After the hook, the individual required annotation must be cleared so a
|
|
// --contents-file-only invocation is not rejected at parse time.
|
|
if cmd.Flags().Lookup("contents").Annotations[cobra.BashCompOneRequiredFlag] != nil {
|
|
t.Fatal("installReportHook should clear the individual required on --contents")
|
|
}
|
|
// And a PreRunE must now be installed to resolve the source.
|
|
if cmd.PreRunE == nil {
|
|
t.Fatal("installReportHook should install a PreRunE")
|
|
}
|
|
}
|
|
|
|
func TestInstallReportHook_NoOpForOtherProduct(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
installReportHook(cmd, "chat", "create_report")
|
|
if cmd.Flags().Lookup("contents").Annotations[cobra.BashCompOneRequiredFlag] == nil {
|
|
t.Fatal("non-report product must not touch required annotation")
|
|
}
|
|
if cmd.PreRunE != nil {
|
|
t.Fatal("non-report product must not install a PreRunE")
|
|
}
|
|
}
|
|
|
|
func TestInstallReportHook_NoOpForOtherReportTool(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
installReportHook(cmd, "report", "get_received_report_list")
|
|
if cmd.PreRunE != nil {
|
|
t.Fatal("non-target report tool must not install a PreRunE")
|
|
}
|
|
}
|
|
|
|
func TestInstallReportHook_ChainsExistingPreRunE(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
originalCalled := false
|
|
cmd.PreRunE = func(*cobra.Command, []string) error { originalCalled = true; return nil }
|
|
installReportHook(cmd, "report", "create_report")
|
|
if err := cmd.Flags().Set("contents", reportContentsPayload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.PreRunE(cmd, nil); err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if !originalCalled {
|
|
t.Fatal("original PreRunE was dropped")
|
|
}
|
|
}
|
|
|
|
func TestInstallReportHook_BailsIfChainedPreRunEFails(t *testing.T) {
|
|
cmd := newReportSubmitStub()
|
|
cmd.PreRunE = func(*cobra.Command, []string) error { return errors.New("original boom") }
|
|
installReportHook(cmd, "report", "create_report")
|
|
err := cmd.PreRunE(cmd, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "original boom") {
|
|
t.Fatalf("expected original PreRunE error to bubble, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallReportHook_NilCmdSafe(t *testing.T) {
|
|
installReportHook(nil, "report", "create_report")
|
|
}
|